From a62518d1c64477960d023fffb65327319ba74fd8 Mon Sep 17 00:00:00 2001 From: Paul Donald Date: Wed, 3 Dec 2025 01:12:14 +0100 Subject: [PATCH] luci-app-filemanager: upgrades -use the new lstat based file listing which is symlink aware -convert spaces to tabs -title action buttons -fixed formatted size prefixes -ES6 conversion -CSS cleanup -code reduction -permissions column Signed-off-by: Paul Donald --- .../resources/view/system/filemanager.js | 2124 ++++++++--------- .../view/system/filemanager/HexEditor.js | 312 +-- .../resources/view/system/filemanager/md.js | 138 +- .../view/system/filemanager/md_help.js | 4 +- 4 files changed, 1180 insertions(+), 1398 deletions(-) diff --git a/applications/luci-app-filemanager/htdocs/luci-static/resources/view/system/filemanager.js b/applications/luci-app-filemanager/htdocs/luci-static/resources/view/system/filemanager.js index 22ec88fa68..d49a759847 100644 --- a/applications/luci-app-filemanager/htdocs/luci-static/resources/view/system/filemanager.js +++ b/applications/luci-app-filemanager/htdocs/luci-static/resources/view/system/filemanager.js @@ -8,6 +8,21 @@ 'require view.system.filemanager.md_help as md_help'; 'require view.system.filemanager.HexEditor as HE'; +const callFileList = rpc.declare({ + object: 'file', + method: 'list', + params: [ 'path' ] +}); + +const fileTypes = { + 'block' : _('Block device'), + 'char' : _('Character device'), + 'directory' : _('Directory'), + 'fifo' : _('FIFO/Pipe'), + 'file' : _('File'), + 'socket' : _('Socket'), + 'symlink' : _('Symlink'), +} function pop(a, message, severity) { ui.addNotification(a, message, severity) @@ -18,24 +33,25 @@ function popTimeout(a, message, timeout, severity) { } // Initialize global variables -var currentPath = '/'; // Current path in the filesystem -var selectedItems = new Set(); // Set of selected files/directories -var sortField = 'name'; // Field to sort files by -var sortDirection = 'asc'; // Sort direction (ascending/descending) -var configFilePath = '/etc/config/filemanager'; // Path to the configuration file +let currentPath = '/'; // Current path in the filesystem +const selectedItems = new Set(); // Set of selected files/directories +let sortField = 'name'; // Field to sort files by +let sortAscending = true; // Sort direction (ascending/descending) +let configFilePath = '/etc/config/filemanager'; // Path to the configuration file // Initialize drag counter -var dragCounter = 0; +let dragCounter = 0; // Configuration object to store interface settings -var config = { +let config = { // Column widths in the file table columnWidths: { 'name': 150, 'type': 100, 'size': 100, 'mtime': 150, - 'actions': 100 + 'permissions': 70, + 'actions': 100, }, // Minimum column widths @@ -44,7 +60,8 @@ var config = { 'type': 80, 'size': 80, 'mtime': 120, - 'actions': 80 + 'permissions': 70, + 'actions': 80, }, // Maximum column widths @@ -53,7 +70,8 @@ var config = { 'type': 200, 'size': 200, 'mtime': 300, - 'actions': 200 + 'permissions': 70, + 'actions': 200, }, // Padding and window sizes @@ -61,46 +79,39 @@ var config = { paddingMin: 5, paddingMax: 20, currentDirectory: '/', // Current directory - windowSizes: { - width: 800, - height: 400 - }, - editorContainerSizes: { - text: { - width: 850, - height: 550 - }, - hex: { - width: 850, - height: 550 - } - }, + windowHeight: 800, + windowWidth: 400, - otherSettings: {} // Additional settings + texteditorHeight: 550, + texteditorWidth: 850, + hexeditorHeight: 550, + hexeditorWidth: 850, + + // otherSettings: {} // Additional settings }; // Function to upload a file to the server function uploadFile(filename, filedata, onProgress) { return new Promise(function(resolve, reject) { - var formData = new FormData(); + let formData = new FormData(); formData.append('sessionid', rpc.getSessionID()); // Add session ID formData.append('filename', filename); // File name including path formData.append('filedata', filedata); // File data - var xhr = new XMLHttpRequest(); + let xhr = new XMLHttpRequest(); xhr.open('POST', L.env.cgi_base + '/cgi-upload', true); // Configure the request // Monitor upload progress xhr.upload.onprogress = function(event) { if (event.lengthComputable && onProgress) { - var percent = (event.loaded / event.total) * 100; + let percent = (event.loaded / event.total) * 100; onProgress(percent); // Call the progress callback with percentage } }; // Handle request completion - xhr.onload = function() { + xhr.onload = () => { if (xhr.status === 200) { resolve(xhr.responseText); // Upload successful } else { @@ -109,7 +120,7 @@ function uploadFile(filename, filedata, onProgress) { }; // Handle network errors - xhr.onerror = function() { + xhr.onerror = () => { reject(new Error('Network error')); }; @@ -155,35 +166,8 @@ async function loadConfig() { config[key][k] = parseInt(v, 10); }); break; - - case 'currentDirectory': - config.currentDirectory = value; - break; - - case 'windowSizes': - parseKeyValuePairs(value, ':', (k, v) => { - config.windowSizes = config.windowSizes || {}; - const sizeValue = parseInt(v, 10); - if (!isNaN(sizeValue)) { - config.windowSizes[k] = sizeValue; - } - }); - break; - case 'editorContainerSizes': - parseKeyValuePairs(value, ':', (mode, sizeStr) => { - const [widthStr, heightStr] = sizeStr.split('x'); - const width = parseInt(widthStr, 10); - const height = parseInt(heightStr, 10); - if (!isNaN(width) && !isNaN(height)) { - config.editorContainerSizes[mode] = { - width: width, - height: height - }; - } - }); - break; default: - config[key] = value; + config[key] = isNaN(value) ? value : parseInt(value, 10); } }); }); @@ -194,52 +178,35 @@ async function loadConfig() { // Function to save settings to the configuration file function saveConfig() { - // Before saving, ensure sizes are valid - ['text', 'hex'].forEach(function(mode) { - var sizes = config.editorContainerSizes[mode]; - if (!sizes || isNaN(sizes.width) || isNaN(sizes.height) || sizes.width <= 0 || sizes.height <= 0) { - // Use default sizes if invalid - config.editorContainerSizes[mode] = { - width: 850, - height: 550 - }; - } - }); - var configLines = ['config filemanager', - '\toption columnWidths \'' + Object.keys(config.columnWidths).map(function(field) { + let configLines = ['config filemanager', + '\toption columnWidths \'' + Object.keys(config.columnWidths).map((field) => { return field + ':' + config.columnWidths[field]; }).join(',') + '\'', - '\toption columnMinWidths \'' + Object.keys(config.columnMinWidths).map(function(field) { + '\toption columnMinWidths \'' + Object.keys(config.columnMinWidths).map((field) => { return field + ':' + config.columnMinWidths[field]; }).join(',') + '\'', - '\toption columnMaxWidths \'' + Object.keys(config.columnMaxWidths).map(function(field) { + '\toption columnMaxWidths \'' + Object.keys(config.columnMaxWidths).map((field) => { return field + ':' + config.columnMaxWidths[field]; }).join(',') + '\'', '\toption padding \'' + config.padding + '\'', '\toption paddingMin \'' + config.paddingMin + '\'', '\toption paddingMax \'' + config.paddingMax + '\'', '\toption currentDirectory \'' + config.currentDirectory + '\'', - '\toption windowSizes \'' + Object.keys(config.windowSizes).map(function(key) { - return key + ':' + config.windowSizes[key]; - }).join(',') + '\'', - '\toption editorContainerSizes \'' + Object.keys(config.editorContainerSizes).map(function(mode) { - var sizes = config.editorContainerSizes[mode]; - return mode + ':' + sizes.width + 'x' + sizes.height; - }).join(',') + '\'' + '\toption windowHeight \'' + config.windowHeight + '\'', + '\toption windowWidth \'' + config.windowWidth + '\'', + '\toption texteditorWidth \'' + config.texteditorWidth + '\'', + '\toption texteditorHeight \'' + config.texteditorHeight + '\'', + '\toption hexeditorWidth \'' + config.hexeditorWidth + '\'', + '\toption hexeditorHeight \'' + config.hexeditorHeight + '\'', ]; - // Add additional settings - Object.keys(config.otherSettings).forEach(function(key) { - configLines.push('\toption ' + key + ' \'' + config.otherSettings[key] + '\''); - }); - - var configContent = configLines.join('\n') + '\n'; + const configContent = configLines.join('\n') + '\n'; // Write settings to file - return fs.write(configFilePath, configContent).then(function() { + return fs.write(configFilePath, configContent).then(() => { return Promise.resolve(); - }).catch(function(err) { + }).catch((err) => { return Promise.reject(new Error('Failed to save configuration: ' + err.message)); }); } @@ -249,443 +216,361 @@ function joinPath(path, name) { return path.endsWith('/') ? path + name : path + '/' + name; } -// Function to convert symbolic permissions to numeric format -function symbolicToNumeric(permissions) { - var specialPerms = 0; - var permMap = { - 'r': 4, - 'w': 2, - 'x': 1, - '-': 0 - }; - var numeric = ''; - for (var i = 0; i < permissions.length; i += 3) { - var subtotal = 0; - for (var j = 0; j < 3; j++) { - var char = permissions[i + j]; - if (char === 's' || char === 'S') { - // Special setuid and setgid bits - if (i === 0) { - specialPerms += 4; - } else if (i === 3) { - specialPerms += 2; - } - subtotal += permMap['x']; - } else if (char === 't' || char === 'T') { - // Special sticky bit - if (i === 6) { - specialPerms += 1; - } - subtotal += permMap['x']; - } else { - subtotal += permMap[char] !== undefined ? permMap[char] : 0; - } - } - numeric += subtotal.toString(); - } - if (specialPerms > 0) { - numeric = specialPerms.toString() + numeric; - } - return numeric; +function modeToRwx(mode) { + const perms = mode & 0o777; // extract permission bits + + const toRwx = n => + ((n & 4) ? 'r' : '-') + + ((n & 2) ? 'w' : '-') + + ((n & 1) ? 'x' : '-'); + + const owner = toRwx((perms >> 6) & 0b111); + const group = toRwx((perms >> 3) & 0b111); + const world = toRwx(perms & 0b111); + + return `${owner}${group}${world}`; +} + + +function modeToOctal(mode) { + const perms = mode & 0o777; + return perms.toString(8); } // Function to get a list of files in a directory function getFileList(path) { - return fs.exec('/bin/ls', ['-lA', '--full-time', path]).then(function(res) { - // If there is an error and no any info about files, reject - if (res.code !== 0 && (!res.stdout || !res.stdout.trim())) { - var errorMessage = res.stderr ? res.stderr.trim() : 'Unknown error'; - return Promise.reject(new Error('Failed to list directory: ' + errorMessage)); - } - var stdout = res.stdout || ''; - var lines = stdout.trim().split('\n'); - var files = []; - lines.forEach(function(line) { - if (line.startsWith('total') || !line.trim()) return; - // Ignore ls error lines (common in /proc) - if (line.startsWith('ls:')) return; - // Parse the output line from 'ls' command - var parts = line.match(/^([\-dlpscbD])([rwxstST-]{9})\s+\d+\s+(\S+)\s+(\S+)\s+(\d+(?:,\s*\d+)?)\s+([\d]{4}-[\d]{2}-[\d]{2}\s+\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?\s+[+-]\d{4})\s+(.+)$/); - if (!parts || parts.length < 7) { - console.warn('Failed to parse line:', line); - return; - } - var typeChar = parts[1]; - var permissions = line.substring(0, 10); - var owner = parts[3]; - var group = parts[4]; - var size = parseInt(parts[5], 10); - var dateStr = parts[6]; - var name = parts[7]; - var type = ''; - var target = null; - if (typeChar === 'd') { - type = 'directory'; // Directory - } else if (typeChar === '-') { - type = 'file'; // File - } else if (typeChar === 'l') { - type = 'symlink'; - const idx = name.indexOf(' -> '); - if (idx >= 0) { - target = name.slice(idx + 4); - name = name.slice(0, idx); - } - else { - // SYMLINK WITHOUT TARGET (case /proc//exe) - target = null; - } - } else if (typeChar === 'c') { - type = 'character device'; // Character device - } else if (typeChar === 'b') { - type = 'block device'; // Block device - } else if (typeChar === 'p') { - type = 'named pipe'; // Named pipe - } else if (typeChar === 's') { - type = 'socket'; // Socket - } else { - type = 'unknown'; // Unknown type - } - var mtime = Date.parse(dateStr); - if (type === 'symlink' && target && size === 4096) { - size = -1; // Size for symlinks may be incorrect - } + return callFileList(path).then((res) => { + const files = []; + res?.entries?.forEach((file) => { files.push({ - name: name, - type: type, - size: size, - mtime: mtime / 1000, - owner: owner, - group: group, - permissions: permissions.substring(1), - numericPermissions: symbolicToNumeric(permissions.substring(1)), - target: target + ...file, + permissions: modeToRwx(file.mode), + numericPermissions: modeToOctal(file.mode), }); }); + return files; }); } // Function to insert CSS styles into the document function insertCss(cssContent) { - var styleElement = document.createElement('style'); + const styleElement = document.createElement('style'); styleElement.type = 'text/css'; styleElement.appendChild(document.createTextNode(cssContent)); document.head.appendChild(styleElement); } // CSS styles for the file manager interface -var cssContent = ` +const cssContent = ` .cbi-button-apply, .cbi-button-reset, .cbi-button-save:not(.custom-save-button) { - display: none !important; + display: none !important; } .cbi-page-actions { - background: none !important; - border: none !important; - padding: ${config.padding}px 0 !important; - margin: 0 !important; - display: flex; - justify-content: flex-start; - margin-top: 10px; + background: none !important; + border: none !important; + padding: ${config.padding}px 0 !important; + margin: 0 !important; + display: flex; + justify-content: flex-start; + margin-top: 10px; } .cbi-tabmenu { - background: none !important; - border: none !important; - margin: 0 !important; - padding: 0 !important; + background: none !important; + border: none !important; + margin: 0 !important; + padding: 0 !important; } .cbi-tabmenu li { - display: inline-block; - margin-right: 10px; + display: inline-block; + margin-right: 10px; } #file-list-container { - margin-top: 30px !important; - overflow: auto; - border: 1px solid #ccc; - padding: 0; - min-width: 600px; - position: relative; - resize: both; + margin-top: 30px !important; + overflow: auto; + border: 1px solid #ccc; + padding: 0; + min-width: 600px; + position: relative; + resize: both; } #file-list-container.drag-over { - border: 2px dashed #00BFFF; - background-color: rgba(0, 191, 255, 0.1); + border: 2px dashed #00BFFF; + background-color: rgba(0, 191, 255, 0.1); } /* Add extra space to the left of the Name and Type columns */ .table th:nth-child(1), .table td:nth-child(1), /* Name column */ .table th:nth-child(2), .table td:nth-child(2) { /* Type column */ - padding-left: 5px; /* Adjust this value for the desired spacing */ + padding-left: 5px; /* Adjust this value for the desired spacing */ } /* Add extra space to the right of the Size column */ .table th:nth-child(3), .table td:nth-child(3) { /* Size column */ - padding-right: 5px; /* Adjust this value for the desired spacing */ + padding-right: 5px; /* Adjust this value for the desired spacing */ } /* Add extra space to the left of the Size column header */ .table th:nth-child(3) { /* Size column header */ - padding-left: 15px; /* Adjust this value for the desired spacing */ + padding-left: 15px; /* Adjust this value for the desired spacing */ } #drag-overlay { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: rgba(0, 191, 255, 0.2); - display: flex; - align-items: center; - justify-content: center; - font-size: 24px; - color: #00BFFF; - z-index: 10; - pointer-events: none; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 191, 255, 0.2); + display: flex; + align-items: center; + justify-content: center; + font-size: 24px; + color: #00BFFF; + z-index: 10; + pointer-events: none; } #content-editor { - margin-top: 30px !important; + margin-top: 30px !important; } .editor-container { - display: flex; - flex-direction: column; - resize: both; - overflow: hidden; + display: flex; + flex-direction: column; + resize: both; + overflow: hidden; } .editor-content { - flex: 1; - display: flex; - overflow: hidden; + flex: 1; + display: flex; + overflow: hidden; } .line-numbers { - width: 50px; - background-color: #f0f0f0; - text-align: right; - padding-right: 5px; - user-select: none; - border-right: 1px solid #ccc; - overflow: hidden; - flex-shrink: 0; - -ms-overflow-style: none; /* Hide scrollbar in IE и Edge */ - scrollbar-width: none; /* Hide scrollbar in Firefox */ + width: 50px; + background-color: #f0f0f0; + text-align: right; + padding-right: 5px; + user-select: none; + border-right: 1px solid #ccc; + overflow: hidden; + flex-shrink: 0; + -ms-overflow-style: none; /* Hide scrollbar in IE и Edge */ + scrollbar-width: none; /* Hide scrollbar in Firefox */ } .line-numbers::-webkit-scrollbar { - display: none; /* Hide scrollbar in Chrome, Safari и Opera */ + display: none; /* Hide scrollbar in Chrome, Safari и Opera */ } .line-numbers div { - font-family: monospace; - font-size: 14px; - line-height: 1.2em; - height: 1.2em; + font-family: monospace; + font-size: 14px; + line-height: 1.2em; + height: 1.2em; } #editor-message { - font-size: 18px; - font-weight: bold; + font-size: 18px; + font-weight: bold; } #editor-textarea { - flex: 1; - resize: none; - border: none; - font-family: monospace; - font-size: 14px; - line-height: 1.2em; - padding: 0; - margin: 0; - overflow: auto; - box-sizing: border-box; + flex: 1; + resize: none; + border: none; + font-family: monospace; + font-size: 14px; + line-height: 1.2em; + padding: 0; + margin: 0; + overflow: auto; + box-sizing: border-box; } #editor-textarea, .line-numbers { - overflow-y: scroll; + overflow-y: scroll; } th { - text-align: left !important; - position: sticky; - top: 0; - border-right: 1px solid #ddd; - box-sizing: border-box; - padding-right: 30px; - white-space: nowrap; - min-width: 100px; - background-color: #fff; - z-index: 2; + text-align: left !important; + position: sticky; + top: 0; + border-right: 1px solid #ddd; + box-sizing: border-box; + padding-right: 30px; + white-space: nowrap; + min-width: 100px; + background-color: #fff; + z-index: 2; } td { - text-align: left !important; - border-right: 1px solid #ddd; - box-sizing: border-box; - white-space: nowrap; - min-width: 100px; - overflow: hidden; - text-overflow: ellipsis; + text-align: left !important; + border-right: 1px solid #ddd; + box-sizing: border-box; + white-space: nowrap; + min-width: 100px; + overflow: hidden; + text-overflow: ellipsis; } tr:hover { - background-color: #f0f0f0 !important; + background-color: #f0f0f0 !important; } .download-button { - color: green; - cursor: pointer; - margin-left: 5px; + color: green; + cursor: pointer; + margin-left: 5px; } .delete-button { - color: red; - cursor: pointer; - margin-left: 5px; + color: red; + cursor: pointer; + margin-left: 5px; } .edit-button { - color: blue; - cursor: pointer; - margin-left: 5px; + color: blue; + cursor: pointer; + margin-left: 5px; } .duplicate-button { - color: orange; - cursor: pointer; - margin-left: 5px; + color: orange; + cursor: pointer; + margin-left: 5px; } .symlink { - color: green; + color: green; } .status-link { - color: blue; - text-decoration: underline; - cursor: pointer; + color: blue; + text-decoration: underline; + cursor: pointer; } .action-button { - margin-right: 10px; - cursor: pointer; + margin-right: 10px; + cursor: pointer; } .size-cell { - text-align: right; - font-family: monospace; - box-sizing: border-box; - white-space: nowrap; - display: flex; - justify-content: flex-end; - align-items: center; + font-family: monospace; + box-sizing: border-box; + white-space: nowrap; + align-items: center; } .size-number { - display: inline-block; - width: 8ch; - text-align: right; + display: inline-block; + width: 8ch; + text-align: right; } .size-unit { - display: inline-block; - width: 4ch; - text-align: right; - margin-left: 0.5ch; + display: inline-block; + width: 4ch; + text-align: right; + margin-left: 0.5ch; } .table { - table-layout: fixed; - border-collapse: collapse; - white-space: nowrap; - width: 100%; + table-layout: fixed; + border-collapse: collapse; + white-space: nowrap; + width: 100%; } .table th:nth-child(3), .table td:nth-child(3) { - width: 100px; - min-width: 100px; - max-width: 500px; + width: 100px; + min-width: 100px; + max-width: 500px; } .table th:nth-child(3) + th, .table td:nth-child(3) + td { - padding-left: 10px; + padding-left: 10px; } .resizer { - position: absolute; - right: 0; - top: 0; - width: 5px; - height: 100%; - cursor: col-resize; - user-select: none; - z-index: 3; + position: absolute; + right: 0; + top: 0; + width: 5px; + height: 100%; + cursor: col-resize; + user-select: none; + z-index: 3; } .resizer::after { - content: ""; - position: absolute; - right: 2px; - top: 0; - width: 1px; - height: 100%; - background: #aaa; -} -#file-list-container.resizable { - resize: both; - overflow: auto; + content: ""; + position: absolute; + right: 2px; + top: 0; + width: 1px; + height: 100%; + background: #aaa; +} +#file-list-container.resizeable { + resize: both; + overflow: auto; } .sort-button { - position: absolute; - right: 10px; - top: 50%; - transform: translateY(-50%); - background: none; - border: 1px solid #ccc; /* Add a visible border */ - color: #fff; /* White text color for better contrast on dark backgrounds */ - cursor: pointer; - padding: 2px 5px; /* Add padding for better clickability */ - font-size: 12px; /* Set font size */ - border-radius: 4px; /* Rounded corners for a better appearance */ - background-color: rgba(0, 0, 0, 0.5); /* Semi-transparent black background */ - transition: background-color 0.3s, color 0.3s; /* Smooth transition effects for hover */ + position: absolute; + right: 10px; + top: 50%; + transform: translateY(-50%); + background: none; + border: 1px solid #ccc; /* Add a visible border */ + color: #fff; /* White text color for better contrast on dark backgrounds */ + cursor: pointer; + padding: 2px 5px; /* Add padding for better clickability */ + font-size: 12px; /* Set font size */ + border-radius: 4px; /* Rounded corners for a better appearance */ + background-color: rgba(0, 0, 0, 0.5); /* Semi-transparent black background */ + transition: background-color 0.3s, color 0.3s; /* Smooth transition effects for hover */ } .sort-button:hover { - background-color: #fff; /* Change background to white on hover */ - color: #000; /* Change text color to black on hover */ - border-color: #fff; /* White border on hover */ + background-color: #fff; /* Change background to white on hover */ + color: #000; /* Change text color to black on hover */ + border-color: #fff; /* White border on hover */ } .sort-button:focus { - outline: none; + outline: none; } #status-bar { - margin-top: 10px; - padding: 10px; - background-color: #f9f9f9; - border: 1px solid #ccc; - min-height: 40px; - display: flex; - align-items: center; - justify-content: space-between; + margin-top: 10px; + padding: 10px; + background-color: #f9f9f9; + border: 1px solid #ccc; + min-height: 40px; + display: flex; + align-items: center; + justify-content: space-between; } #status-info { - font-weight: bold; - display: flex; - align-items: center; + font-weight: bold; + display: flex; + align-items: center; } #status-progress { - width: 50%; + width: 50%; } .cbi-progressbar { - width: 100%; - background-color: #e0e0e0; - border-radius: 5px; - overflow: hidden; - height: 10px; + width: 100%; + background-color: #e0e0e0; + border-radius: 5px; + overflow: hidden; + height: 10px; } .cbi-progressbar div { - height: 100%; - background-color: #76c7c0; - width: 0%; - transition: width 0.2s; + height: 100%; + background-color: #76c7c0; + width: 0%; + transition: width 0.2s; } .file-manager-header { - display: flex; - align-items: center; + display: flex; + align-items: center; } .file-manager-header h2 { - margin: 0; + margin: 0; } .file-manager-header input { - margin-left: 10px; - width: 100%; - max-width: 700px; - font-size: 18px; + margin-left: 10px; + width: 100%; + max-width: 700px; + font-size: 18px; } .file-manager-header button { - margin-left: 10px; - font-size: 18px; + margin-left: 10px; + font-size: 18px; } .directory-link { - /* Choose a color with good contrast or let the theme decide */ - color: #00BFFF; /* DeepSkyBlue */ - font-weight: bold; + /* Choose a color with good contrast or let the theme decide */ + color: #00BFFF; /* DeepSkyBlue */ + font-weight: bold; } .file-link { - color: inherit; /* Use the default text color */ + color: inherit; /* Use the default text color */ } `; @@ -695,20 +580,19 @@ return view.extend({ editorMode: 'text', hexEditorInstance: null, // Method called when the view is loaded - load: function() { - var self = this; - return loadConfig().then(function() { + load() { + const self = this; + return loadConfig().then(() => { currentPath = config.currentDirectory || '/'; return getFileList(currentPath); // Load the file list for the current directory }); }, // Method to render the interface - render: function(data) { - var self = this; + render(data) { + const self = this; insertCss(cssContent); // Insert CSS styles - // insertCss(hexeditCssContent); // Insert hexedit CSS styles - var viewContainer = E('div', { + const viewContainer = E('div', { 'id': 'file-manager-container' }, [ // File Manager Header @@ -721,7 +605,7 @@ return view.extend({ 'id': 'path-input', 'value': currentPath, 'style': 'margin-left: 10px;', - 'keydown': function(event) { + 'keydown'(event) { if (event.key === 'Enter') { self.handleGoButtonClick(); // Trigger directory navigation on Enter } @@ -793,12 +677,12 @@ return view.extend({ 'style': 'display:block;' }, [ // File List Container with Drag-and-Drop - (function() { + (() => { // Create the container for the file list and drag-and-drop functionality - var fileListContainer = E('div', { + const fileListContainer = E('div', { 'id': 'file-list-container', - 'class': 'resizable', - 'style': 'width: ' + config.windowSizes.width + 'px; height: ' + config.windowSizes.height + 'px;' + 'class': 'resizeable', + 'style': 'width: ' + config.windowWidth + 'px; height: ' + config.windowHeight + 'px;' }, [ E('table', { 'class': 'table', @@ -819,6 +703,19 @@ return view.extend({ 'class': 'resizer' }) ]), + E('th', { + 'data-field': 'permissions' + }, [ + _('Permissions'), + E('button', { + 'class': 'sort-button', + 'data-field': 'permissions', + 'title': _('Sort by Permissions') + }, '↕'), + E('div', { + 'class': 'resizer' + }) + ]), E('th', { 'data-field': 'type' }, [ @@ -858,7 +755,7 @@ return view.extend({ 'class': 'resizer' }) ]), - E('th', {}, [ + E('th', {'data-field': 'actions'}, [ E('input', { 'type': 'checkbox', 'id': 'select-all-checkbox', @@ -968,8 +865,8 @@ return view.extend({ E('label', {}, _('Window Width:')), E('input', { 'type': 'number', - 'id': 'window-width-input', - 'value': config.windowSizes.width, + 'id': 'windowWidth-input', + 'value': config.windowWidth, 'style': 'width:100%; margin-bottom:10px;' }) ]), @@ -977,8 +874,8 @@ return view.extend({ E('label', {}, _('Window Height:')), E('input', { 'type': 'number', - 'id': 'window-height-input', - 'value': config.windowSizes.height, + 'id': 'windowHeight-input', + 'value': config.windowHeight, 'style': 'width:100%; margin-bottom:10px;' }) ]), @@ -986,8 +883,8 @@ return view.extend({ E('label', {}, _('Text Editor Width:')), E('input', { 'type': 'number', - 'id': 'editor-text-width-input', - 'value': config.editorContainerSizes.text.width, + 'id': 'texteditorWidth-input', + 'value': config.texteditorWidth, 'style': 'width:100%; margin-bottom:10px;' }) ]), @@ -995,8 +892,8 @@ return view.extend({ E('label', {}, _('Text Editor Height:')), E('input', { 'type': 'number', - 'id': 'editor-text-height-input', - 'value': config.editorContainerSizes.text.height, + 'id': 'texteditorHeight-input', + 'value': config.texteditorHeight, 'style': 'width:100%; margin-bottom:10px;' }) ]), @@ -1004,8 +901,8 @@ return view.extend({ E('label', {}, _('Hex Editor Width:')), E('input', { 'type': 'number', - 'id': 'editor-hex-width-input', - 'value': config.editorContainerSizes.hex.width, + 'id': 'hexeditorWidth-input', + 'value': config.hexeditorWidth, 'style': 'width:100%; margin-bottom:10px;' }) ]), @@ -1013,8 +910,8 @@ return view.extend({ E('label', {}, _('Hex Editor Height:')), E('input', { 'type': 'number', - 'id': 'editor-hex-height-input', - 'value': config.editorContainerSizes.hex.height, + 'id': 'hexeditorHeight-input', + 'value': config.hexeditorHeight, 'style': 'width:100%; margin-bottom:10px;' }) ]), @@ -1022,10 +919,8 @@ return view.extend({ E('label', {}, _('Column Widths (format: name:width,type:width,...):')), E('input', { 'type': 'text', - 'id': 'column-widths-input', - 'value': Object.keys(config.columnWidths).map(function(field) { - return field + ':' + config.columnWidths[field]; - }).join(','), + 'id': 'columnWidths-input', + 'value': Object.values(config.columnWidths).join(''), 'style': 'width:100%; margin-bottom:10px;' }) ]), @@ -1033,10 +928,8 @@ return view.extend({ E('label', {}, _('Column Min Widths (format: name:minWidth,type:minWidth,...):')), E('input', { 'type': 'text', - 'id': 'column-min-widths-input', - 'value': Object.keys(config.columnMinWidths).map(function(field) { - return field + ':' + config.columnMinWidths[field]; - }).join(','), + 'id': 'columnMinWidths-input', + 'value': Object.values(config.columnMinWidths).join(''), 'style': 'width:100%; margin-bottom:10px;' }) ]), @@ -1044,10 +937,8 @@ return view.extend({ E('label', {}, _('Column Max Widths (format: name:maxWidth,type:maxWidth,...):')), E('input', { 'type': 'text', - 'id': 'column-max-widths-input', - 'value': Object.keys(config.columnMaxWidths).map(function(field) { - return field + ':' + config.columnMaxWidths[field]; - }).join(','), + 'id': 'columnMaxWidths-input', + 'value': Object.values(config.columnMaxWidths).join(''), 'style': 'width:100%; margin-bottom:10px;' }) ]), @@ -1064,7 +955,7 @@ return view.extend({ E('label', {}, _('Padding Min:')), E('input', { 'type': 'number', - 'id': 'padding-min-input', + 'id': 'paddingMin-input', 'value': config.paddingMin, 'style': 'width:100%; margin-bottom:10px;' }) @@ -1073,7 +964,7 @@ return view.extend({ E('label', {}, _('Padding Max:')), E('input', { 'type': 'number', - 'id': 'padding-max-input', + 'id': 'paddingMax-input', 'value': config.paddingMax, 'style': 'width:100%; margin-bottom:10px;' }) @@ -1082,7 +973,7 @@ return view.extend({ E('label', {}, _('Current Directory:')), E('input', { 'type': 'text', - 'id': 'current-directory-input', + 'id': 'currentDirectory-input', 'value': config.currentDirectory, 'style': 'width:100%; margin-bottom:10px;' }) @@ -1102,32 +993,32 @@ return view.extend({ ]) ]); // Add event listeners - var sortButtons = viewContainer.querySelectorAll('.sort-button[data-field]'); - sortButtons.forEach(function(button) { - button.addEventListener('click', function(event) { + const sortButtons = viewContainer.querySelectorAll('.sort-button[data-field]'); + sortButtons.forEach((button) => { + button.addEventListener('click', (event) => { event.preventDefault(); - var field = button.getAttribute('data-field'); + const field = button.getAttribute('data-field'); if (field) { self.sortBy(field); // Sort the file list by the selected field } }); }); - // Load the file list and initialize resizable columns - this.loadFileList(currentPath).then(function() { - self.initResizableColumns(); - var fileListContainer = document.getElementById('file-list-container'); + // Load the file list and initialize resizeable columns + this.loadFileList(currentPath).then(() => { + self.initResizeableColumns(); + const fileListContainer = document.getElementById('file-list-container'); if (fileListContainer && typeof ResizeObserver !== 'undefined') { // Initialize ResizeObserver only once if (!self.fileListResizeObserver) { - self.fileListResizeObserver = new ResizeObserver(function(entries) { - for (var entry of entries) { - var newWidth = entry.contentRect.width; - var newHeight = entry.contentRect.height; + self.fileListResizeObserver = new ResizeObserver((entries) => { + for (let entry of entries) { + const newWidth = entry.contentRect.width; + const newHeight = entry.contentRect.height; // Update config only if newWidth and newHeight are greater than 0 if (newWidth > 0 && newHeight > 0) { - config.windowSizes.width = newWidth; - config.windowSizes.height = newHeight; + config.windowWidth = newWidth; + config.windowHeight = newHeight; } } }); @@ -1139,7 +1030,7 @@ return view.extend({ }, // Handler for the "Select All" checkbox click - handleSelectAllClick: function(ev) { + handleSelectAllClick(ev) { if (ev.altKey) { ev.preventDefault(); // Prevent the default checkbox behavior this.handleInvertSelection(); @@ -1149,11 +1040,11 @@ return view.extend({ }, // Function to invert selection - handleInvertSelection: function() { - var allCheckboxes = document.querySelectorAll('.select-checkbox'); - allCheckboxes.forEach(function(checkbox) { + handleInvertSelection() { + const allCheckboxes = document.querySelectorAll('.select-checkbox'); + allCheckboxes.forEach((checkbox) => { checkbox.checked = !checkbox.checked; - var filePath = checkbox.getAttribute('data-file-path'); + const filePath = checkbox.getAttribute('data-file-path'); if (checkbox.checked) { selectedItems.add(filePath); } else { @@ -1171,18 +1062,18 @@ return view.extend({ * * @param {string} tab - The identifier of the tab to switch to ('filemanager', 'editor', 'settings', or 'help'). */ - switchToTab: function(tab) { + switchToTab(tab) { // Retrieve the content containers for each tab - var fileManagerContent = document.getElementById('content-filemanager'); - var editorContent = document.getElementById('content-editor'); - var settingsContent = document.getElementById('content-settings'); - var helpContent = document.getElementById('content-help'); + const fileManagerContent = document.getElementById('content-filemanager'); + const editorContent = document.getElementById('content-editor'); + const settingsContent = document.getElementById('content-settings'); + const helpContent = document.getElementById('content-help'); // Retrieve the tab elements - var tabFileManager = document.getElementById('tab-filemanager'); - var tabEditor = document.getElementById('tab-editor'); - var tabSettings = document.getElementById('tab-settings'); - var tabHelp = document.getElementById('tab-help'); + const tabFileManager = document.getElementById('tab-filemanager'); + const tabEditor = document.getElementById('tab-editor'); + const tabSettings = document.getElementById('tab-settings'); + const tabHelp = document.getElementById('tab-help'); // Ensure all necessary elements are present if (fileManagerContent && editorContent && settingsContent && helpContent && tabFileManager && tabEditor && tabSettings && tabHelp) { @@ -1203,8 +1094,8 @@ return view.extend({ // Reload and display the updated file list when the File Manager tab is activated this.loadFileList(currentPath) .then(() => { - // Initialize resizable columns after successfully loading the file list - this.initResizableColumns(); + // Initialize resizeable columns after successfully loading the file list + this.initResizeableColumns(); }) .catch((err) => { // Display an error notification if loading the file list fails @@ -1224,23 +1115,23 @@ return view.extend({ /** * Renders the Help content by converting Markdown to HTML and inserting it into the Help container. */ - renderHelp: function() { - var self = this; + renderHelp() { + const self = this; // Convert Markdown to HTML - var helpContentHTML = md.parseMarkdown(md_help.helpContentMarkdown); + const helpContentHTML = md.parseMarkdown(md_help.helpContentMarkdown); // Get the Help content container - var helpContent = document.getElementById('content-help'); + const helpContent = document.getElementById('content-help'); if (helpContent) { // Insert the converted HTML into the Help container helpContent.innerHTML = helpContentHTML; - // Initialize resizable functionality for the Help window - self.initResizableHelp(); + // Initialize resizeable functionality for the Help window + self.initResizeableHelp(); } else { console.error('Help content container not found.'); pop(null, E('p', _('Failed to render Help content: Container not found.')), 'error'); @@ -1248,10 +1139,10 @@ return view.extend({ }, /** - * Initializes the resizable functionality for the Help window. + * Initializes the resizeable functionality for the Help window. */ - initResizableHelp: function() { - var helpContent = document.getElementById('content-help'); + initResizeableHelp() { + const helpContent = document.getElementById('content-help'); if (helpContent) { // Set initial dimensions @@ -1266,8 +1157,8 @@ return view.extend({ // Optional: Add a drag handle for better user experience /* var dragHandle = E('div', { - 'class': 'resize-handle', - 'style': 'width: 10px; height: 10px; background: #ccc; position: absolute; bottom: 0; right: 0; cursor: se-resize;' + 'class': 'resize-handle', + 'style': 'width: 10px; height: 10px; background: #ccc; position: absolute; bottom: 0; right: 0; cursor: se-resize;' }); helpContent.appendChild(dragHandle); */ @@ -1277,35 +1168,35 @@ return view.extend({ }, // Handler for the "Go" button click to navigate to a directory - handleGoButtonClick: function() { + handleGoButtonClick() { // Logic to navigate to the specified directory and update the file list - var self = this; - var pathInput = document.getElementById('path-input'); + const self = this; + const pathInput = document.getElementById('path-input'); if (pathInput) { - var newPath = pathInput.value.trim() || '/'; - fs.stat(newPath).then(function(stat) { + const newPath = pathInput.value.trim() || '/'; + fs.stat(newPath).then((stat) => { if (stat.type === 'directory') { currentPath = newPath; pathInput.value = currentPath; - self.loadFileList(currentPath).then(function() { - self.initResizableColumns(); + self.loadFileList(currentPath).then(() => { + self.initResizeableColumns(); }); } else { pop(null, E('p', _('The specified path does not appear to be a directory.')), 'error'); } - }).catch(function(err) { + }).catch((err) => { pop(null, E('p', _('Failed to access the specified path: %s').format(err.message)), 'error'); }); } }, // Handler for dragging files over the drop zone - handleDragEnter: function(event) { + handleDragEnter(event) { event.preventDefault(); event.stopPropagation(); dragCounter++; - var fileListContainer = document.getElementById('file-list-container'); - var dragOverlay = document.getElementById('drag-overlay'); + const fileListContainer = document.getElementById('file-list-container'); + const dragOverlay = document.getElementById('drag-overlay'); if (fileListContainer && dragOverlay) { fileListContainer.classList.add('drag-over'); dragOverlay.style.display = 'flex'; @@ -1313,20 +1204,20 @@ return view.extend({ }, // Handler for when files are over the drop zone - handleDragOver: function(event) { + handleDragOver(event) { event.preventDefault(); event.stopPropagation(); event.dataTransfer.dropEffect = 'copy'; // Indicate copy action }, // Handler for leaving the drop zone - handleDragLeave: function(event) { + handleDragLeave(event) { event.preventDefault(); event.stopPropagation(); dragCounter--; if (dragCounter === 0) { - var fileListContainer = document.getElementById('file-list-container'); - var dragOverlay = document.getElementById('drag-overlay'); + const fileListContainer = document.getElementById('file-list-container'); + const dragOverlay = document.getElementById('drag-overlay'); if (fileListContainer && dragOverlay) { fileListContainer.classList.remove('drag-over'); dragOverlay.style.display = 'none'; @@ -1335,14 +1226,14 @@ return view.extend({ }, // Handler for dropping files into the drop zone - handleDrop: function(event) { + handleDrop(event) { event.preventDefault(); event.stopPropagation(); dragCounter = 0; // Reset counter - var self = this; - var files = event.dataTransfer.files; - var fileListContainer = document.getElementById('file-list-container'); - var dragOverlay = document.getElementById('drag-overlay'); + const self = this; + const files = event.dataTransfer.files; + const fileListContainer = document.getElementById('file-list-container'); + const dragOverlay = document.getElementById('drag-overlay'); if (fileListContainer && dragOverlay) { fileListContainer.classList.remove('drag-over'); dragOverlay.style.display = 'none'; @@ -1353,15 +1244,15 @@ return view.extend({ }, // Handler for uploading a file - handleUploadClick: function(ev) { - var self = this; - var fileInput = document.createElement('input'); + handleUploadClick(ev) { + const self = this; + const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.multiple = true; // Allow selecting multiple files fileInput.style.display = 'none'; document.body.appendChild(fileInput); - fileInput.onchange = function(event) { - var files = event.target.files; + fileInput.onchange = (event) => { + const files = event.target.files; if (!files || files.length === 0) { pop(null, E('p', _('No file selected.')), 'error'); return; @@ -1371,30 +1262,30 @@ return view.extend({ fileInput.click(); }, - uploadFiles: function(files) { - var self = this; - var directoryPath = currentPath; - var statusInfo = document.getElementById('status-info'); - var statusProgress = document.getElementById('status-progress'); - var totalFiles = files.length; - var uploadedFiles = 0; + uploadFiles(files) { + const self = this; + const directoryPath = currentPath; + const statusInfo = document.getElementById('status-info'); + const statusProgress = document.getElementById('status-progress'); + const totalFiles = files.length; + let uploadedFiles = 0; function uploadNextFile(index) { if (index >= totalFiles) { - self.loadFileList(currentPath).then(function() { - self.initResizableColumns(); + self.loadFileList(currentPath).then(() => { + self.initResizeableColumns(); }); return; } - var file = files[index]; - var fullFilePath = joinPath(directoryPath, file.name); + const file = files[index]; + const fullFilePath = joinPath(directoryPath, file.name); if (statusInfo) { statusInfo.textContent = _('Uploading: "%s"...').format(file.name); } if (statusProgress) { statusProgress.innerHTML = ''; - var progressBarContainer = E('div', { + const progressBarContainer = E('div', { 'class': 'cbi-progressbar', 'title': '0%' }, [E('div', { @@ -1403,15 +1294,15 @@ return view.extend({ statusProgress.appendChild(progressBarContainer); } - uploadFile(fullFilePath, file, function(percent) { + uploadFile(fullFilePath, file, (percent) => { if (statusProgress) { - var progressBar = statusProgress.querySelector('.cbi-progressbar div'); + const progressBar = statusProgress.querySelector('.cbi-progressbar div'); if (progressBar) { progressBar.style.width = percent.toFixed(2) + '%'; statusProgress.querySelector('.cbi-progressbar').setAttribute('title', percent.toFixed(2) + '%'); } } - }).then(function() { + }).then(() => { if (statusProgress) { statusProgress.innerHTML = ''; } @@ -1421,7 +1312,7 @@ return view.extend({ popTimeout(null, E('p', _('File "%s" uploaded successfully.').format(file.name)), 5000, 'info'); uploadedFiles++; uploadNextFile(index + 1); - }).catch(function(err) { + }).catch((err) => { if (statusProgress) { statusProgress.innerHTML = ''; } @@ -1436,27 +1327,27 @@ return view.extend({ }, // Handler for creating a directory - handleMakeDirectoryClick: function(ev) { + handleMakeDirectoryClick(ev) { // Logic to create a new directory - var self = this; - var statusInfo = document.getElementById('status-info'); - var statusProgress = document.getElementById('status-progress'); + const self = this; + const statusInfo = document.getElementById('status-info'); + const statusProgress = document.getElementById('status-progress'); if (statusInfo && statusProgress) { statusInfo.innerHTML = ''; statusProgress.innerHTML = ''; - var dirNameInput = E('input', { + const dirNameInput = E('input', { 'type': 'text', 'placeholder': _('Directory Name'), 'style': 'margin-right: 10px;' }); - var saveButton = E('button', { + const saveButton = E('button', { 'class': 'btn', 'disabled': true, - 'click': function() { + 'click'() { self.createDirectory(dirNameInput.value); } }, _('Save')); - dirNameInput.addEventListener('input', function() { + dirNameInput.addEventListener('input', () => { if (dirNameInput.value.trim()) { saveButton.disabled = false; } else { @@ -1470,50 +1361,50 @@ return view.extend({ }, // Function to create a directory - createDirectory: function(dirName) { + createDirectory(dirName) { // Execute the 'mkdir' command and update the interface - var self = this; - var trimmedDirName = dirName.trim(); - var dirPath = joinPath(currentPath, trimmedDirName); - fs.exec('mkdir', [dirPath]).then(function(res) { + const self = this; + const trimmedDirName = dirName.trim(); + const dirPath = joinPath(currentPath, trimmedDirName); + fs.exec('mkdir', [dirPath]).then((res) => { if (res.code !== 0) { return Promise.reject(new Error(res.stderr.trim())); } popTimeout(null, E('p', _('Directory "%s" created successfully.').format(trimmedDirName)), 5000, 'info'); - self.loadFileList(currentPath).then(function() { - self.initResizableColumns(); + self.loadFileList(currentPath).then(() => { + self.initResizeableColumns(); }); - var statusInfo = document.getElementById('status-info'); - var statusProgress = document.getElementById('status-progress'); + const statusInfo = document.getElementById('status-info'); + const statusProgress = document.getElementById('status-progress'); if (statusInfo) statusInfo.textContent = _('No directory selected.'); if (statusProgress) statusProgress.innerHTML = ''; - }).catch(function(err) { + }).catch((err) => { pop(null, E('p', _('Failed to create directory "%s": %s').format(trimmedDirName, err.message)), 'error'); }); }, // Handler for creating a file - handleCreateFileClick: function(ev) { + handleCreateFileClick(ev) { // Logic to create a new file - var self = this; - var statusInfo = document.getElementById('status-info'); - var statusProgress = document.getElementById('status-progress'); + const self = this; + const statusInfo = document.getElementById('status-info'); + const statusProgress = document.getElementById('status-progress'); if (statusInfo && statusProgress) { statusInfo.innerHTML = ''; statusProgress.innerHTML = ''; - var fileNameInput = E('input', { + const fileNameInput = E('input', { 'type': 'text', 'placeholder': _('File Name'), 'style': 'margin-right: 10px;' }); - var createButton = E('button', { + const createButton = E('button', { 'class': 'btn', 'disabled': true, - 'click': function() { + 'click'() { self.createFile(fileNameInput.value); } }, _('Create')); - fileNameInput.addEventListener('input', function() { + fileNameInput.addEventListener('input', () => { if (fileNameInput.value.trim()) { createButton.disabled = false; } else { @@ -1527,355 +1418,346 @@ return view.extend({ }, // Function to create a file - createFile: function(fileName) { + createFile(fileName) { // Execute the 'touch' command and update the interface - var self = this; - var trimmedFileName = fileName.trim(); - var filePath = joinPath(currentPath, trimmedFileName); - fs.exec('touch', [filePath]).then(function(res) { + const self = this; + const trimmedFileName = fileName.trim(); + const filePath = joinPath(currentPath, trimmedFileName); + fs.exec('touch', [filePath]).then((res) => { if (res.code !== 0) { return Promise.reject(new Error(res.stderr.trim())); } popTimeout(null, E('p', _('File "%s" created successfully.').format(trimmedFileName)), 5000, 'info'); - self.loadFileList(currentPath).then(function() { - self.initResizableColumns(); + self.loadFileList(currentPath).then(() => { + self.initResizeableColumns(); }); - var statusInfo = document.getElementById('status-info'); - var statusProgress = document.getElementById('status-progress'); + const statusInfo = document.getElementById('status-info'); + const statusProgress = document.getElementById('status-progress'); if (statusInfo) statusInfo.textContent = _('No file selected.'); if (statusProgress) statusProgress.innerHTML = ''; - }).catch(function(err) { + }).catch((err) => { pop(null, E('p', _('Failed to create file "%s": %s').format(trimmedFileName, err.message)), 'error'); }); }, // Handler for checkbox state change on a file - handleCheckboxChange: function(ev) { - // Update the set of selected items - var checkbox = ev.target; - var filePath = checkbox.getAttribute('data-file-path'); - if (checkbox.checked) { - selectedItems.add(filePath); - } else { - selectedItems.delete(filePath); - } + handleCheckboxChange(ev) { + const cb = ev.target; + const filePath = cb.dataset.filePath; + + cb.checked + ? selectedItems.add(filePath) + : selectedItems.delete(filePath); + this.updateDeleteSelectedButton(); this.updateSelectAllCheckbox(); }, // Update the "Delete Selected" button - updateDeleteSelectedButton: function() { - // Show or hide the button based on the number of selected items - var deleteSelectedButton = document.getElementById('delete-selected-button'); - if (deleteSelectedButton) { - if (selectedItems.size > 0) { - deleteSelectedButton.style.display = ''; - } else { - deleteSelectedButton.style.display = 'none'; - } - } + updateDeleteSelectedButton() { + const btn = document.getElementById('delete-selected-button'); + if (!btn) return; + + btn.style.display = selectedItems.size > 0 ? '' : 'none'; }, // Update the "Select All" checkbox state - updateSelectAllCheckbox: function() { - var selectAllCheckbox = document.getElementById('select-all-checkbox'); - var allCheckboxes = document.querySelectorAll('.select-checkbox'); - var totalCheckboxes = allCheckboxes.length; - var checkedCheckboxes = 0; - allCheckboxes.forEach(function(checkbox) { - if (checkbox.checked) { - checkedCheckboxes++; - } - }); - if (selectAllCheckbox) { - if (checkedCheckboxes === 0) { - selectAllCheckbox.checked = false; - selectAllCheckbox.indeterminate = false; - } else if (checkedCheckboxes === totalCheckboxes) { - selectAllCheckbox.checked = true; - selectAllCheckbox.indeterminate = false; - } else { - selectAllCheckbox.checked = false; - selectAllCheckbox.indeterminate = true; - } + updateSelectAllCheckbox() { + const selectAll = document.getElementById('select-all-checkbox'); + if (!selectAll) return; + + const checkboxes = [...document.querySelectorAll('.select-checkbox')]; + if (checkboxes.length === 0) { + selectAll.checked = false; + selectAll.indeterminate = false; + return; } + + const total = checkboxes.length; + const checked = checkboxes.filter(cb => cb.checked).length; + + selectAll.checked = checked === total; + selectAll.indeterminate = checked > 0 && checked < total; }, // Handler for the "Select All" checkbox change - handleSelectAllChange: function(ev) { - // Logic to select or deselect all files - var self = this; - var selectAllCheckbox = ev.target; - var allCheckboxes = document.querySelectorAll('.select-checkbox'); + handleSelectAllChange(ev) { + const checked = ev.target.checked; + const checkboxes = [...document.querySelectorAll('.select-checkbox')]; + selectedItems.clear(); - allCheckboxes.forEach(function(checkbox) { - checkbox.checked = selectAllCheckbox.checked; - var filePath = checkbox.getAttribute('data-file-path'); - if (selectAllCheckbox.checked) { - selectedItems.add(filePath); - } + + checkboxes.forEach(cb => { + cb.checked = checked; + if (checked) selectedItems.add(cb.dataset.filePath); }); + this.updateDeleteSelectedButton(); + this.updateSelectAllCheckbox(); }, // Handler for deleting selected items - handleDeleteSelected: function() { + handleDeleteSelected() { // Delete selected files and directories - var self = this; + const self = this; if (selectedItems.size === 0) { return; } if (!confirm(_('Are you sure you want to delete the selected files and directories?'))) { return; } - var promises = []; - selectedItems.forEach(function(filePath) { - promises.push(fs.remove(filePath).catch(function(err) { + const promises = []; + selectedItems.forEach((filePath) => { + promises.push(fs.remove(filePath).catch((err) => { pop(null, E('p', _('Failed to delete %s: %s').format(filePath, err.message)), 'error'); })); }); - Promise.all(promises).then(function() { + Promise.all(promises).then(() => { popTimeout(null, E('p', _('Selected files and directories deleted successfully.')), 5000, 'info'); selectedItems.clear(); self.updateDeleteSelectedButton(); - self.loadFileList(currentPath).then(function() { - self.initResizableColumns(); + self.loadFileList(currentPath).then(() => { + self.initResizeableColumns(); }); - }).catch(function(err) { + }).catch((err) => { pop(null, E('p', _('Failed to delete selected files and directories: %s').format(err.message)), 'error'); }); }, // Function to load the file list - loadFileList: function(path) { - // Get the list of files and display them in the table - var self = this; + loadFileList(path) { + const self = this; selectedItems.clear(); - return getFileList(path).then(function(files) { - var fileList = document.getElementById('file-list'); + + return getFileList(path).then(files => { + // 1. Get column order dynamically from table header + const columns = Array.from( + document.querySelectorAll('#file-table thead th[data-field]') + ).map(th => th.getAttribute('data-field')); + + + const fileList = document.getElementById('file-list'); if (!fileList) { pop(null, E('p', _('Failed to display the file list.')), 'error'); return; } + fileList.innerHTML = ''; files.sort(self.compareFiles.bind(self)); + + // + // Add ".." parent row + // if (path !== '/') { - var parentPath = path.substring(0, path.lastIndexOf('/')) || '/'; - var listItemUp = E('tr', { + const parentPath = path.substring(0, path.lastIndexOf('/')) || '/'; + + const tr = E('tr', { 'data-file-path': parentPath, 'data-file-type': 'directory' - }, [E('td', { - 'colspan': 5 - }, [E('a', { - 'href': '#', - 'click': function() { - self.handleDirectoryClick(parentPath); + }); + + // Create cells for *every* column + for (const col of columns) { + if (col === 'name') { + tr.appendChild( + E('td', { colspan: columns.length }, [ + E('a', { + href: '#', + click: () => self.handleDirectoryClick(parentPath) + }, '.. (Parent Directory)') + ]) + ); + break; + } else { + tr.appendChild(E('td')); // empty cell } - }, '.. (Parent Directory)')])]); - fileList.appendChild(listItemUp); + } + + fileList.appendChild(tr); } - files.forEach(function(file) { - var listItem; - var displaySize = (file.type === 'directory' || (file.type === 'symlink' && file.size === -1)) ? -1 : file.size; - var checkbox = E('input', { - 'type': 'checkbox', - 'class': 'select-checkbox', - 'data-file-path': joinPath(path, file.name), - 'change': function(ev) { - self.handleCheckboxChange(ev); - } + + // + // 2. For each file, create row dynamically + // + for (const file of files) { + const fullPath = joinPath(path, file.name); + const tr = E('tr', { + 'data-file-path': fullPath, + 'data-file-type': file.type, + 'data-permissions': file.permissions, + 'data-numeric-permissions': file.numericPermissions, + 'data-owner': file?.user || file.uid, + 'data-group': file?.group || file.gid, + 'data-size': file.size }); - var actionButtons = [checkbox, E('span', { - 'class': 'edit-button', - 'click': function() { - self.handleEditFile(joinPath(path, file.name), file); - } - }, '✏️'), E('span', { - 'class': 'duplicate-button', - 'click': function() { - self.handleDuplicateFile(joinPath(path, file.name), file); - } - }, '📑'), E('span', { - 'class': 'delete-button', - 'click': function() { - self.handleDeleteFile(joinPath(path, file.name), file); + + // + // Prebuild common reusable items + // + const nameLink = E('a', { + href: '#', + title: file.permissions, + class: `${file.type}-link`, + click(event) { + if (file.type === 'directory' || file?.target?.type === 'directory') { + self.handleDirectoryClick(fullPath); + } else { + event.preventDefault(); + self.handleFileClick(fullPath, event.altKey ? 'hex' : 'text'); + } } - }, '🗑️')]; + }, file?.target ? `${file.name} → ${file.target?.name}` : file.name); + + const actions = []; + const checkbox = E('input', { + type: 'checkbox', + class: 'select-checkbox', + 'data-file-path': fullPath, + change: ev => self.handleCheckboxChange(ev) + }); + actions.push(checkbox); + + actions.push(E('span', { + class: 'edit-button', + title: _('Edit properties'), + click: () => self.handleEditFile(fullPath, file) + }, '✏️')); + + actions.push(E('span', { + class: 'duplicate-button', + title: _('Duplicate'), + click: () => self.handleDuplicateFile(fullPath, file) + }, '📑')); + + actions.push(E('span', { + class: 'delete-button', + title: _('Delete'), + click: () => self.handleDeleteFile(fullPath, file) + }, '🗑️')); + if (file.type === 'file') { - actionButtons.push(E('span', { - 'class': 'download-button', - 'click': function() { - self.handleDownloadFile(joinPath(path, file.name)); - } + actions.push(E('span', { + class: 'download-button', + title: _('Download'), + click: () => self.handleDownloadFile(fullPath) }, '⬇️')); } - var actionTd = E('td', {}, actionButtons); - if (file.type === 'directory') { - listItem = E('tr', { - 'data-file-path': joinPath(path, file.name), - 'data-file-type': 'directory', - 'data-permissions': file.permissions, - 'data-numeric-permissions': file.numericPermissions, - 'data-owner': file.owner, - 'data-group': file.group, - 'data-size': -1 - }, [E('td', {}, [E('a', { - 'href': '#', - 'class': 'directory-link', - 'click': function() { - self.handleDirectoryClick(joinPath(path, file.name)); - } - }, file.name)]), E('td', {}, _('Directory')), E('td', { - 'class': 'size-cell' - }, [E('span', { - 'class': 'size-number' - }, '-'), E('span', { - 'class': 'size-unit' - }, '')]), E('td', {}, new Date(file.mtime * 1000).toLocaleString()), actionTd]); - } else if (file.type === 'file') { - listItem = E('tr', { - 'data-file-path': joinPath(path, file.name), - 'data-file-type': 'file', - 'data-permissions': file.permissions, - 'data-numeric-permissions': file.numericPermissions, - 'data-owner': file.owner, - 'data-group': file.group, - 'data-size': file.size - }, [E('td', {}, [E('a', { - 'href': '#', - 'class': 'file-link', - 'click': function() { - event.preventDefault(); // Prevent the default link behavior - if (event.altKey) { - self.handleFileClick(joinPath(path, file.name), 'hex'); // Open in hex editor + + // + // 3. Build `` dynamically based on column definitions + // + for (const col of columns) { + let td; + + switch (col) { + case 'name': + td = E('td', {}, [nameLink]); + break; + + case 'type': + td = E('td', {}, fileTypes[file.type] || file.type); + break; + + case 'size': + if (file.type === 'directory' || (file.type === 'symlink' && file.size === -1)) { + td = E('td', { class: 'size-cell' }, [ + E('span', { class: 'size-number' }, '-'), + E('span', { class: 'size-unit' }, ''), + ]); } else { - self.handleFileClick(joinPath(path, file.name), 'text'); // Open in text editor + const formatted = self.getFormattedSize(file.size); + td = E('td', { class: 'size-cell' }, [ + E('span', { class: 'size-number' }, formatted.number), + E('span', { class: 'size-unit' }, formatted.unit) + ]); } - } - }, file.name)]), E('td', {}, _('File')), E('td', { - 'class': 'size-cell' - }, [E('span', { - 'class': 'size-number' - }, self.getFormattedSize(file.size).number), E('span', { - 'class': 'size-unit' - }, self.getFormattedSize(file.size).unit)]), E('td', {}, new Date(file.mtime * 1000).toLocaleString()), actionTd]); - } else if (file.type === 'symlink') { - var symlinkName = file.target ? (file.name + ' -> ' + file.target) : file.name; - var symlinkSize = (file.size === -1) ? -1 : file.size; - var sizeContent; - if (symlinkSize >= 0) { - var formattedSize = self.getFormattedSize(symlinkSize); - sizeContent = [E('span', { - 'class': 'size-number' - }, formattedSize.number), E('span', { - 'class': 'size-unit' - }, formattedSize.unit)]; - } else { - sizeContent = [E('span', { - 'class': 'size-number' - }, '-'), E('span', { - 'class': 'size-unit' - }, '')]; + break; + + case 'mtime': + td = E('td', {}, new Date(file.mtime * 1000).toLocaleString()); + break; + + case 'actions': + td = E('td', {}, actions); + break; + + case 'permissions': + td = E('td', {}, file.permissions); + break; + + default: + // Support future dynamically-added columns + td = E('td', {}, file[col] ?? ''); + break; } - listItem = E('tr', { - 'data-file-path': joinPath(path, file.name), - 'data-file-type': 'symlink', - 'data-symlink-target': file.target, - 'data-permissions': file.permissions, - 'data-numeric-permissions': file.numericPermissions, - 'data-owner': file.owner, - 'data-group': file.group, - 'data-size': symlinkSize - }, [E('td', {}, [E('a', { - 'href': '#', - 'class': 'symlink-name', - 'click': function() { - event.preventDefault(); // Prevent the default link behavior - if (event.altKey) { - self.handleSymlinkClick(joinPath(path, file.name), file.target, 'hex'); // Open target in hex editor - } else { - self.handleSymlinkClick(joinPath(path, file.name), file.target, 'text'); - } - } - }, symlinkName)]), E('td', {}, _('Symlink')), E('td', { - 'class': 'size-cell' - }, sizeContent), E('td', {}, new Date(file.mtime * 1000).toLocaleString()), actionTd]); - } else { - listItem = E('tr', { - 'data-file-path': joinPath(path, file.name), - 'data-file-type': file.type - }, [E('td', {}, file.name), E('td', {}, file.type.charAt(0).toUpperCase() + file.type.slice(1)), E('td', { - 'class': 'size-cell' - }, [E('span', { - 'class': 'size-number' - }, '-'), E('span', { - 'class': 'size-unit' - }, '')]), E('td', {}, new Date(file.mtime * 1000).toLocaleString()), actionTd]); - } - if (listItem && listItem instanceof Node) { - fileList.appendChild(listItem); - } else { - console.error('listItem is not a Node:', listItem); + + tr.appendChild(td); } - }); - self.setInitialColumnWidths(); - var statusInfo = document.getElementById('status-info'); - var statusProgress = document.getElementById('status-progress'); - if (statusInfo) { - statusInfo.textContent = _('No file selected.'); - } - if (statusProgress) { - statusProgress.innerHTML = ''; + + fileList.appendChild(tr); } + + // + // housekeeping + // + const statusInfo = document.getElementById('status-info'); + const statusProgress = document.getElementById('status-progress'); + + if (statusInfo) statusInfo.textContent = _('No file selected.'); + if (statusProgress) statusProgress.innerHTML = ''; + + self.setInitialColumnWidths(); self.updateSelectAllCheckbox(); self.updateDeleteSelectedButton(); return Promise.resolve(); - }).catch(function(err) { + }).catch((err) => { pop(null, E('p', _('Failed to load file list: %s').format(err.message)), 'error'); return Promise.reject(err); }); }, // Function to format file size - getFormattedSize: function(size) { - // Convert the size to a human-readable format (KB, MB, GB) - var units = [' ', 'k', 'M', 'G']; - var unitIndex = 0; - var formattedSize = size; - while (formattedSize >= 1024 && unitIndex < units.length - 1) { - formattedSize /= 1024; - unitIndex++; - } - formattedSize = formattedSize.toFixed(2); - if (size === 0) { - formattedSize = '0.00'; - unitIndex = 0; + getFormattedSize(size) { + /* 64 bit systems i.e. rpcd have max size of 128 TB */ + const units = [' ', 'K', 'M', 'G', 'T']; + let index = 0; + let value = size; + + if (size > 0) { + // Keep dividing until below 1024 or no more units + while (value >= 1024 && index < units.length - 1) { + value /= 1024; + index++; + } } - formattedSize = formattedSize.toString().padStart(6, ' '); + + // Format to 2 decimals, always 6 chars wide + const num = value.toFixed(2).padStart(6, ' '); + return { - number: formattedSize, - unit: ' ' + units[unitIndex] + 'B' + number: num, + unit: ' ' + units[index] + 'B' }; }, // Function to sort files - sortBy: function(field) { + sortBy(field) { // Change the sort field and direction, and reload the file list if (sortField === field) { - sortDirection = (sortDirection === 'asc') ? 'desc' : 'asc'; + sortAscending = !sortAscending; } else { sortField = field; - sortDirection = 'asc'; + sortAscending = true; } this.loadFileList(currentPath); }, // Function to compare files for sorting - compareFiles: function(a, b) { + compareFiles(a, b) { // Compare files based on the selected field and direction - var order = (sortDirection === 'asc') ? 1 : -1; - var aValue = a[sortField]; - var bValue = b[sortField]; + const order = sortAscending ? 1 : -1; + let aValue = a[sortField]; + let bValue = b[sortField]; if (sortField === 'size') { aValue = (a.type === 'directory' || (a.type === 'symlink' && a.size === -1)) ? -1 : a.size; bValue = (b.type === 'directory' || (b.type === 'symlink' && b.size === -1)) ? -1 : b.size; @@ -1886,25 +1768,25 @@ return view.extend({ }, // Set initial column widths in the table - setInitialColumnWidths: function() { + setInitialColumnWidths() { // Apply column width settings to the file table - var table = document.getElementById('file-table'); + const table = document.getElementById('file-table'); if (!table) { return; } - var headers = table.querySelectorAll('th'); - headers.forEach(function(header, index) { - var field = header.getAttribute('data-field'); + const headers = table.querySelectorAll('th'); + headers.forEach((header, index) => { + const field = header.getAttribute('data-field'); if (field && config.columnWidths[field]) { - var width = config.columnWidths[field]; - var minWidth = config.columnMinWidths[field] || 50; - var maxWidth = config.columnMaxWidths[field] || 500; + const width = config.columnWidths[field]; + const minWidth = config.columnMinWidths[field] || 50; + const maxWidth = config.columnMaxWidths[field] || 500; header.style.width = width + 'px'; header.style.minWidth = minWidth + 'px'; header.style.maxWidth = maxWidth + 'px'; - var rows = table.querySelectorAll('tr'); - rows.forEach(function(row, rowIndex) { - var cell = row.children[index]; + const rows = table.querySelectorAll('tr'); + rows.forEach((row, rowIndex) => { + const cell = row.children[index]; if (cell) { cell.style.width = width + 'px'; cell.style.minWidth = minWidth + 'px'; @@ -1916,16 +1798,16 @@ return view.extend({ }, // Handler for clicking on a directory - handleDirectoryClick: function(newPath) { + handleDirectoryClick(newPath) { // Navigate to the selected directory and update the file list - var self = this; + const self = this; currentPath = newPath || '/'; - var pathInput = document.getElementById('path-input'); + const pathInput = document.getElementById('path-input'); if (pathInput) { pathInput.value = currentPath; } - this.loadFileList(currentPath).then(function() { - self.initResizableColumns(); + this.loadFileList(currentPath).then(() => { + self.initResizeableColumns(); }); }, @@ -1935,7 +1817,7 @@ return view.extend({ * @param {Uint8Array} uint8Array - The binary data to check. * @returns {boolean} - Returns true if the data is UTF-8 text, false otherwise. */ - isText: function(uint8Array) { + isText(uint8Array) { const len = uint8Array.length; let i = 0; @@ -1984,7 +1866,7 @@ return view.extend({ }, // Function to handle clicking on a file to open it in the editor - handleFileClick: function(filePath, mode) { + handleFileClick(filePath, mode) { const self = this; const fileRow = document.querySelector(`tr[data-file-path='${filePath}']`); const editorMessage = document.getElementById('editor-message'); @@ -2031,100 +1913,83 @@ return view.extend({ }); }, // Adjust padding for line numbers in the editor - adjustLineNumbersPadding: function() { + adjustLineNumbersPadding() { // Update padding based on scrollbar size - var lineNumbersDiv = document.getElementById('line-numbers'); - var editorTextarea = document.getElementById('editor-textarea'); + const lineNumbersDiv = document.getElementById('line-numbers'); + const editorTextarea = document.getElementById('editor-textarea'); if (!lineNumbersDiv || !editorTextarea) { return; } - var scrollbarHeight = editorTextarea.offsetHeight - editorTextarea.clientHeight; + const scrollbarHeight = editorTextarea.offsetHeight - editorTextarea.clientHeight; lineNumbersDiv.style.paddingBottom = scrollbarHeight + 'px'; }, // Handler for downloading a file - handleDownloadFile: function(filePath) { + handleDownloadFile(filePath) { // Download the file to the user's local machine - var self = this; - var fileName = filePath.split('/').pop(); + const self = this; + const fileName = filePath.split('/').pop(); // Use the read_direct method to download the file fs.read_direct(filePath, 'blob') - .then(function(blob) { + .then((blob) => { if (!(blob instanceof Blob)) { throw new Error(_('Response is not a Blob')); } - var url = window.URL.createObjectURL(blob); - var a = document.createElement('a'); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); a.href = url; a.download = fileName; document.body.appendChild(a); a.click(); a.remove(); window.URL.revokeObjectURL(url); - }).catch(function(err) { + }).catch((err) => { pop(null, E('p', _('Failed to download file "%s": %s').format(fileName, err.message)), 'error'); }); }, // Handler for deleting a file - handleDeleteFile: function(filePath, fileInfo) { + handleDeleteFile(filePath, fileInfo) { // Delete the selected file or directory - var self = this; - var itemTypeLabel = ''; - var itemName = filePath.split('/').pop(); - - if (fileInfo && fileInfo.type) { - if (fileInfo.type === 'directory') { - itemTypeLabel = _('directory'); - } else if (fileInfo.type === 'file') { - itemTypeLabel = _('file'); - } else if (fileInfo.type === 'symlink') { - itemTypeLabel = _('symbolic link'); - } else { - itemTypeLabel = _('item'); - } - } else { - itemTypeLabel = _('item'); - } + const self = this; + const itemName = filePath.split('/').pop(); + const itemTypeLabel = fileTypes[fileInfo?.type]; if (confirm(_('Are you sure you want to delete this %s: "%s"?').format(itemTypeLabel, itemName))) { - fs.remove(filePath).then(function() { + fs.remove(filePath).then(() => { popTimeout(null, E('p', _('Successfully deleted %s: "%s".').format(itemTypeLabel, itemName)), 5000, 'info'); - self.loadFileList(currentPath).then(function() { - self.initResizableColumns(); + self.loadFileList(currentPath).then(() => { + self.initResizeableColumns(); }); - var statusInfo = document.getElementById('status-info'); + const statusInfo = document.getElementById('status-info'); if (statusInfo) { statusInfo.textContent = _('Deleted %s: "%s".').format(itemTypeLabel, itemName); } - }).catch(function(err) { + }).catch((err) => { pop(null, E('p', _('Failed to delete %s "%s": %s').format(itemTypeLabel, itemName, err.message)), 'error'); }); } }, // Update line numbers in the text editor - updateLineNumbers: function() { + updateLineNumbers() { // Update the line numbers display when the text changes - var lineNumbersDiv = document.getElementById('line-numbers'); - var editorTextarea = document.getElementById('editor-textarea'); - if (!lineNumbersDiv || !editorTextarea) { - return; - } - var content = editorTextarea.value; - var lines = content.split('\n').length; - var lineNumbersContent = ''; - for (var i = 1; i <= lines; i++) { - lineNumbersContent += '
' + i + '
'; - } - lineNumbersDiv.innerHTML = lineNumbersContent; + const lineNumbersDiv = document.getElementById('line-numbers'); + const editorTextarea = document.getElementById('editor-textarea'); + if (!lineNumbersDiv || !editorTextarea) return; + + // Count lines + const lineCount = editorTextarea.value.split('\n').length; + + // Build HTML using join — much faster than concatenation + lineNumbersDiv.innerHTML = Array.from({ length: lineCount }, (_, i) => `
${i + 1}
`).join(''); }, // Synchronize scrolling between line numbers and text - syncScroll: function() { + syncScroll() { // Sync scrolling of line numbers with the text area - var lineNumbersDiv = document.getElementById('line-numbers'); - var editorTextarea = document.getElementById('editor-textarea'); + const lineNumbersDiv = document.getElementById('line-numbers'); + const editorTextarea = document.getElementById('editor-textarea'); if (!lineNumbersDiv || !editorTextarea) { return; } @@ -2132,7 +1997,7 @@ return view.extend({ }, // Toggle line numbers display in the editor - toggleLineNumbers: function() { + toggleLineNumbers() { // Ensure the editor is in Text Mode before toggling line numbers if (this.editorMode !== 'text') { console.warn('Toggle Line Numbers is only available in Text Mode.'); @@ -2140,8 +2005,8 @@ return view.extend({ } // Get the line numbers div and the textarea - var lineNumbersDiv = document.getElementById('line-numbers'); - var editorTextarea = document.getElementById('editor-textarea'); + const lineNumbersDiv = document.getElementById('line-numbers'); + const editorTextarea = document.getElementById('editor-textarea'); if (!lineNumbersDiv || !editorTextarea) { console.error('Line numbers div or editor textarea not found.'); return; @@ -2160,38 +2025,38 @@ return view.extend({ }, // Generate a name for a copy of a file - getCopyName: function(originalName, existingNames) { - // Create a new unique file name based on the original - var dotIndex = originalName.lastIndexOf('.'); - var namePart, extension; - if (dotIndex > 0 && dotIndex !== originalName.length - 1) { - namePart = originalName.substring(0, dotIndex); - extension = originalName.substring(dotIndex); - } else { - namePart = originalName; - extension = ''; - } - var copyName = namePart + ' (copy)' + extension; - var copyIndex = 1; - while (existingNames.includes(copyName)) { - copyIndex++; - copyName = namePart + ' (copy ' + copyIndex + ')' + extension; + getCopyName(originalName, existingNames) { + // Split filename into base name + extension + const dotIndex = originalName.lastIndexOf('.'); + const hasExt = dotIndex > 0 && dotIndex < originalName.length - 1; + + const base = hasExt ? originalName.slice(0, dotIndex) : originalName; + const ext = hasExt ? originalName.slice(dotIndex) : ''; + + // First attempt: "name (copy).ext" + let candidate = `${base} (copy)${ext}`; + + // If taken, try: "name (copy 2).ext", "name (copy 3).ext", ... + let counter = 2; + while (existingNames.includes(candidate)) { + candidate = `${base} (copy ${counter++})${ext}`; } - return copyName; + + return candidate; }, // Handler for duplicating a file - handleDuplicateFile: function(filePath, fileInfo) { + handleDuplicateFile(filePath, fileInfo) { // Copy the file or directory with a new name - var self = this; - getFileList(currentPath).then(function(files) { - var existingNames = files.map(function(f) { + const self = this; + getFileList(currentPath).then((files) => { + const existingNames = files.map((f) => { return f.name; }); - var newName = self.getCopyName(fileInfo.name, existingNames); - var newPath = joinPath(currentPath, newName); - var command; - var args; + const newName = self.getCopyName(fileInfo.name, existingNames); + const newPath = joinPath(currentPath, newName); + let command; + let args; if (fileInfo.type === 'directory') { command = 'cp'; args = ['-rp', filePath, newPath]; @@ -2202,52 +2067,52 @@ return view.extend({ command = 'cp'; args = ['-p', filePath, newPath]; } - fs.exec(command, args).then(function(res) { + fs.exec(command, args).then((res) => { if (res.code !== 0) { return Promise.reject(new Error(res.stderr.trim())); } popTimeout(null, E('p', _('Successfully duplicated %s "%s" as "%s".').format(_('item'), fileInfo.name, newName)), 5000, 'info'); - self.loadFileList(currentPath).then(function() { - self.initResizableColumns(); + self.loadFileList(currentPath).then(() => { + self.initResizeableColumns(); }); - }).catch(function(err) { + }).catch((err) => { pop(null, E('p', _('Failed to duplicate %s "%s": %s').format(_('item'), fileInfo.name, err.message)), 'error'); }); - }).catch(function(err) { + }).catch((err) => { pop(null, E('p', _('Failed to get file list: %s').format(err.message)), 'error'); }); }, // Handler for saving a file after editing - handleSaveFile: function(filePath) { - var self = this; - var contentBlob; + handleSaveFile(filePath) { + const self = this; + let contentBlob; if (self.editorMode === 'text') { - var textarea = document.querySelector('#editor-container textarea'); + const textarea = document.querySelector('#editor-container textarea'); if (!textarea) { pop(null, E('p', _('Editor textarea not found.')), 'error'); return; } - var content = textarea.value; + const content = textarea.value; self.fileContent = content; // Convert content to Uint8Array in chunks not exceeding 8KB - var CHUNK_SIZE = 8 * 1024; // 8KB - var totalLength = content.length; - var chunks = []; - for (var i = 0; i < totalLength; i += CHUNK_SIZE) { - var chunkStr = content.slice(i, i + CHUNK_SIZE); - var chunkBytes = new TextEncoder().encode(chunkStr); + const CHUNK_SIZE = 8 * 1024; // 8KB + const totalLength = content.length; + let chunks = []; + for (let i = 0; i < totalLength; i += CHUNK_SIZE) { + const chunkStr = content.slice(i, i + CHUNK_SIZE); + const chunkBytes = new TextEncoder().encode(chunkStr); chunks.push(chunkBytes); } // Concatenate chunks into a single Uint8Array - var totalBytes = chunks.reduce(function(prev, curr) { + const totalBytes = chunks.reduce((prev, curr) => { return prev + curr.length; }, 0); - var dataArray = new Uint8Array(totalBytes); - var offset = 0; - chunks.forEach(function(chunk) { + let dataArray = new Uint8Array(totalBytes); + let offset = 0; + chunks.forEach((chunk) => { dataArray.set(chunk, offset); offset += chunk.length; }); @@ -2264,15 +2129,15 @@ return view.extend({ }); } - var statusInfo = document.getElementById('status-info'); - var statusProgress = document.getElementById('status-progress'); - var fileName = filePath.split('/').pop(); + const statusInfo = document.getElementById('status-info'); + const statusProgress = document.getElementById('status-progress'); + const fileName = filePath.split('/').pop(); if (statusInfo) { statusInfo.textContent = _('Saving file: "%s"...').format(fileName); } if (statusProgress) { statusProgress.innerHTML = ''; - var progressBarContainer = E('div', { + const progressBarContainer = E('div', { 'class': 'cbi-progressbar', 'title': '0%' }, [E('div', { @@ -2281,30 +2146,30 @@ return view.extend({ statusProgress.appendChild(progressBarContainer); } - uploadFile(filePath, contentBlob, function(percent) { + uploadFile(filePath, contentBlob, (percent) => { if (statusProgress) { - var progressBar = statusProgress.querySelector('.cbi-progressbar div'); + const progressBar = statusProgress.querySelector('.cbi-progressbar div'); if (progressBar) { progressBar.style.width = percent.toFixed(2) + '%'; statusProgress.querySelector('.cbi-progressbar').setAttribute('title', percent.toFixed(2) + '%'); } } - }).then(function() { - var permissions = self.originalFilePermissions; + }).then(() => { + const permissions = self.originalFilePermissions; if (permissions !== undefined) { - return fs.exec('chmod', [permissions, filePath]).then(function(res) { + return fs.exec('chmod', [permissions, filePath]).then((res) => { if (res.code !== 0) { throw new Error(res.stderr.trim()); } - }).then(function() { + }).then(() => { if (statusInfo) { statusInfo.textContent = _('File "%s" uploaded successfully.').format(fileName); } popTimeout(null, E('p', _('File "%s" uploaded successfully.').format(fileName)), 5000, 'info'); - return self.loadFileList(currentPath).then(function() { - self.initResizableColumns(); + return self.loadFileList(currentPath).then(() => { + self.initResizeableColumns(); }); - }).catch(function(err) { + }).catch((err) => { pop(null, E('p', _('Failed to apply permissions to file "%s": %s').format(fileName, err.message)), 'error'); }); } else { @@ -2312,11 +2177,11 @@ return view.extend({ statusInfo.textContent = _('File "%s" uploaded successfully.').format(fileName); } popTimeout(null, E('p', _('File "%s" uploaded successfully.').format(fileName)), 5000, 'info'); - return self.loadFileList(currentPath).then(function() { - self.initResizableColumns(); + return self.loadFileList(currentPath).then(() => { + self.initResizeableColumns(); }); } - }).catch(function(err) { + }).catch((err) => { if (statusProgress) { statusProgress.innerHTML = ''; } @@ -2327,19 +2192,14 @@ return view.extend({ }); }, - // Handler for clicking on a symbolic link - handleSymlinkClick: function(linkPath, targetPath, mode) { + handleSymlinkClick(linkPath, targetPath, mode) { // Navigate to the target of the symbolic link - var self = this; - if (!targetPath) { - pop(null, E('p', _('The symlink does not have a valid target.')), 'error'); - return; - } + const self = this; if (!targetPath.startsWith('/')) { targetPath = joinPath(currentPath, targetPath); } - fs.stat(targetPath).then(function(stat) { + fs.stat(targetPath).then((stat) => { if (stat.type === 'directory') { self.handleDirectoryClick(targetPath); } else if (stat.type === 'file') { @@ -2347,47 +2207,47 @@ return view.extend({ } else { pop(null, E('p', _('The symlink points to an unsupported type.')), 'error'); } - }).catch(function(err) { + }).catch((err) => { pop(null, E('p', _('Failed to access symlink target: %s').format(err.message)), 'error'); }); - var statusInfo = document.getElementById('status-info'); + const statusInfo = document.getElementById('status-info'); if (statusInfo) { statusInfo.textContent = _('Symlink: ') + linkPath + ' -> ' + targetPath; } }, - // Initialize resizable columns in the table - initResizableColumns: function() { + // Initialize resizeable columns in the table + initResizeableColumns() { // Add handlers to adjust column widths - var self = this; - var table = document.getElementById('file-table'); + const self = this; + const table = document.getElementById('file-table'); if (!table) { return; } - var headers = table.querySelectorAll('th'); - headers.forEach(function(header, index) { - var resizer = header.querySelector('.resizer'); + const headers = table.querySelectorAll('th'); + headers.forEach((header, index) => { + const resizer = header.querySelector('.resizer'); if (resizer) { resizer.removeEventListener('mousedown', header.resizeHandler); - header.resizeHandler = function(e) { + header.resizeHandler = (e) => { e.preventDefault(); - var startX = e.pageX; - var startWidth = header.offsetWidth; - var field = header.getAttribute('data-field'); - var minWidth = config.columnMinWidths[field] || 50; - var maxWidth = config.columnMaxWidths[field] || 500; + const startX = e.pageX; + const startWidth = header.offsetWidth; + const field = header.getAttribute('data-field'); + const minWidth = config.columnMinWidths[field] || 50; + const maxWidth = config.columnMaxWidths[field] || 500; function doDrag(e) { - var currentX = e.pageX; - var newWidth = startWidth + (currentX - startX); + const currentX = e.pageX; + const newWidth = startWidth + (currentX - startX); if (newWidth >= minWidth && newWidth <= maxWidth) { header.style.width = newWidth + 'px'; if (field) { config.columnWidths[field] = newWidth; } - var rows = table.querySelectorAll('tr'); - rows.forEach(function(row, rowIndex) { - var cell = row.children[index]; + const rows = table.querySelectorAll('tr'); + rows.forEach((row, rowIndex) => { + const cell = row.children[index]; if (cell) { cell.style.width = newWidth + 'px'; } @@ -2409,44 +2269,44 @@ return view.extend({ }, // Handler for editing a file's properties (name, permissions, etc.) - handleEditFile: function(filePath, fileInfo) { + handleEditFile(filePath, fileInfo) { // Display a form to edit the file's properties - var self = this; - var statusInfo = document.getElementById('status-info'); - var statusProgress = document.getElementById('status-progress'); + const self = this; + const statusInfo = document.getElementById('status-info'); + const statusProgress = document.getElementById('status-progress'); if (statusInfo && statusProgress) { statusInfo.innerHTML = ''; statusProgress.innerHTML = ''; - var nameInput = E('input', { + const nameInput = E('input', { 'type': 'text', 'value': fileInfo.name, 'placeholder': fileInfo.name, 'style': 'margin-right: 10px;' }); - var permsInput = E('input', { + const permsInput = E('input', { 'type': 'text', 'placeholder': fileInfo.numericPermissions, 'style': 'margin-right: 10px; width: 80px;' }); - var ownerInput = E('input', { + const ownerInput = E('input', { 'type': 'text', - 'placeholder': fileInfo.owner, + 'placeholder': fileInfo?.user || fileInfo.uid, 'style': 'margin-right: 10px; width: 100px;' }); - var groupInput = E('input', { + const groupInput = E('input', { 'type': 'text', - 'placeholder': fileInfo.group, + 'placeholder': fileInfo?.group || fileInfo.gid, 'style': 'margin-right: 10px; width: 100px;' }); - var saveButton = E('button', { + const saveButton = E('button', { 'class': 'btn', 'disabled': true, - 'click': function() { + 'click'() { self.saveFileChanges(filePath, fileInfo, nameInput.value, permsInput.value, ownerInput.value, groupInput.value); } }, _('Save')); - [nameInput, permsInput, ownerInput, groupInput].forEach(function(input) { - input.addEventListener('input', function() { + [nameInput, permsInput, ownerInput, groupInput].forEach((input) => { + input.addEventListener('input', () => { if (nameInput.value !== fileInfo.name || permsInput.value || ownerInput.value || groupInput.value) { saveButton.disabled = false; } else { @@ -2464,280 +2324,184 @@ return view.extend({ }, // Save changes to a file's properties - saveFileChanges: function(filePath, fileInfo, newName, newPerms, newOwner, newGroup) { + saveFileChanges(filePath, fileInfo, newName, newPerms, newOwner, newGroup) { // Apply changes and update the interface - var self = this; - var commands = []; - var originalPath = filePath; - var originalName = fileInfo.name; - var newItemName = newName || originalName; + const self = this; + const commands = []; + const originalPath = filePath; + const originalName = fileInfo.name; + const newItemName = newName || originalName; if (newName && newName !== fileInfo.name) { - var newPath = joinPath(currentPath, newName); + const newPath = joinPath(currentPath, newName); commands.push(['mv', [filePath, newPath]]); filePath = newPath; } if (newPerms) { commands.push(['chmod', [newPerms, filePath]]); } + if (newOwner || newGroup) { - var ownerGroup = ''; - if (newOwner) { - ownerGroup += newOwner; - } else { - ownerGroup += fileInfo.owner; - } - ownerGroup += ':'; - if (newGroup) { - ownerGroup += newGroup; - } else { - ownerGroup += fileInfo.group; - } - commands.push(['chown', [ownerGroup, filePath]]); + const owner = newOwner ?? (fileInfo?.user || fileInfo.uid); + const group = newGroup ?? (fileInfo?.group || fileInfo.gid); + + commands.push(['chown', [`${owner}:${group}`, filePath]]); } - var promise = Promise.resolve(); - commands.forEach(function(cmd) { - promise = promise.then(function() { - return fs.exec(cmd[0], cmd[1]).then(function(res) { + + let promise = Promise.resolve(); + commands.forEach((cmd) => { + promise = promise.then(() => { + return fs.exec(cmd[0], cmd[1]).then((res) => { if (res.code !== 0) { return Promise.reject(new Error(res.stderr.trim())); } }); }); }); - promise.then(function() { + promise.then(() => { popTimeout(null, E('p', _('Changes to %s "%s" uploaded successfully.').format(_('item'), newItemName)), 5000, 'info'); - self.loadFileList(currentPath).then(function() { - self.initResizableColumns(); + self.loadFileList(currentPath).then(() => { + self.initResizeableColumns(); }); - var statusInfo = document.getElementById('status-info'); - var statusProgress = document.getElementById('status-progress'); + const statusInfo = document.getElementById('status-info'); + const statusProgress = document.getElementById('status-progress'); if (statusInfo) statusInfo.textContent = _('No item selected.'); if (statusProgress) statusProgress.innerHTML = ''; - }).catch(function(err) { + }).catch((err) => { pop(null, E('p', _('Failed to save changes to %s "%s": %s').format(_('item'), newItemName, err.message)), 'error'); }); }, - // Handler for saving interface settings - handleSaveSettings: function(ev) { + handleSaveSettings(ev) { ev.preventDefault(); var self = this; - var inputs = { - columnWidths: document.getElementById('column-widths-input'), - columnMinWidths: document.getElementById('column-min-widths-input'), - columnMaxWidths: document.getElementById('column-max-widths-input'), - padding: document.getElementById('padding-input'), - paddingMin: document.getElementById('padding-min-input'), - paddingMax: document.getElementById('padding-max-input'), - currentDirectory: document.getElementById('current-directory-input'), - windowWidth: document.getElementById('window-width-input'), - windowHeight: document.getElementById('window-height-input'), - editorTextWidth: document.getElementById('editor-text-width-input'), - editorTextHeight: document.getElementById('editor-text-height-input'), - editorHexWidth: document.getElementById('editor-hex-width-input'), - editorHexHeight: document.getElementById('editor-hex-height-input') + + const parseAndSetConfig = (configPath, value) => { + const input = document.getElementById(`${configPath}-input`); + if (!input) return; + let v = input.value.trim(); + + if (typeof value == 'object') { + parseKeyValuePairs(v, ':', (k, v) => { + config[configPath][k] = parseInt(v, 10); + }); + + } else + config[configPath] = v; }; - function parseWidthSettings(inputValue, configKey) { - if (!inputValue) return; - inputValue.split(',').forEach(function(widthStr) { - var widthParts = widthStr.split(':'); - if (widthParts.length === 2) { - var field = widthParts[0]; - var width = parseInt(widthParts[1], 10); - if (!isNaN(width)) { - config[configKey][field] = width; - } - } - }); - } - if (inputs.columnWidths && inputs.padding) { - parseWidthSettings(inputs.columnWidths.value.trim(), 'columnWidths'); - parseWidthSettings(inputs.columnMinWidths.value.trim(), 'columnMinWidths'); - parseWidthSettings(inputs.columnMaxWidths.value.trim(), 'columnMaxWidths'); - var paddingValue = parseInt(inputs.padding.value.trim(), 10); - var paddingMinValue = parseInt(inputs.paddingMin.value.trim(), 10); - var paddingMaxValue = parseInt(inputs.paddingMax.value.trim(), 10); - if (!isNaN(paddingValue)) { - config.padding = paddingValue; - } - if (!isNaN(paddingMinValue)) { - config.paddingMin = paddingMinValue; - } - if (!isNaN(paddingMaxValue)) { - config.paddingMax = paddingMaxValue; - } - if (inputs.currentDirectory) { - var currentDirectoryValue = inputs.currentDirectory.value.trim(); - if (currentDirectoryValue) { - config.currentDirectory = currentDirectoryValue; - } - } - if (inputs.windowWidth && inputs.windowHeight) { - var windowWidthValue = parseInt(inputs.windowWidth.value.trim(), 10); - var windowHeightValue = parseInt(inputs.windowHeight.value.trim(), 10); - if (!isNaN(windowWidthValue)) { - config.windowSizes.width = windowWidthValue; - } - if (!isNaN(windowHeightValue)) { - config.windowSizes.height = windowHeightValue; - } - } - if (inputs.editorTextWidth && inputs.editorTextHeight) { - var textWidth = parseInt(inputs.editorTextWidth.value.trim(), 10); - var textHeight = parseInt(inputs.editorTextHeight.value.trim(), 10); - if (!isNaN(textWidth) && !isNaN(textHeight)) { - config.editorContainerSizes.text.width = textWidth; - config.editorContainerSizes.text.height = textHeight; - } - } - if (inputs.editorHexWidth && inputs.editorHexHeight) { - var hexWidth = parseInt(inputs.editorHexWidth.value.trim(), 10); - var hexHeight = parseInt(inputs.editorHexHeight.value.trim(), 10); - if (!isNaN(hexWidth) && !isNaN(hexHeight)) { - config.editorContainerSizes.hex.width = hexWidth; - config.editorContainerSizes.hex.height = hexHeight; - } - } + Object.entries(config).forEach(([configPath, value]) => { + parseAndSetConfig(configPath, value); + }); - saveConfig().then(function() { + saveConfig().then(() => { popTimeout(null, E('p', _('Settings uploaded successfully.')), 5000, 'info'); self.setInitialColumnWidths(); - var styleElement = document.querySelector('style'); + const styleElement = document.querySelector('style'); if (styleElement) { styleElement.textContent = styleElement.textContent.replace(/padding: \d+px/g, 'padding: ' + config.padding + 'px'); } - var fileListContainer = document.getElementById('file-list-container'); + const fileListContainer = document.getElementById('file-list-container'); if (fileListContainer) { - fileListContainer.style.width = config.windowSizes.width + 'px'; - fileListContainer.style.height = config.windowSizes.height + 'px'; + fileListContainer.style.width = config.windowWidth + 'px'; + fileListContainer.style.height = config.windowHeight + 'px'; } currentPath = config.currentDirectory || '/'; - var pathInput = document.getElementById('path-input'); + const pathInput = document.getElementById('path-input'); if (pathInput) { pathInput.value = currentPath; } - self.loadFileList(currentPath).then(function() { - self.initResizableColumns(); + self.loadFileList(currentPath).then(() => { + self.initResizeableColumns(); }); - var editorContainer = document.getElementById('editor-container'); + const editorContainer = document.getElementById('editor-container'); if (editorContainer) { - var editorMode = self.editorMode; - var editorSizes = config.editorContainerSizes[editorMode] || { - width: 850, - height: 550 + const editorMode = self.editorMode; + const editorSizes = { + width: config[`${editorMode}editorWidth`] || 850, + height: config[`${editorMode}editorHeight`] || 550 }; editorContainer.style.width = editorSizes.width + 'px'; editorContainer.style.height = editorSizes.height + 'px'; } - }).catch(function(err) { + }).catch((err) => { pop(null, E('p', _('Failed to save settings: %s').format(err.message)), 'error'); }); - } }, // Load settings into the settings form - // Load settings into the settings form - loadSettings: function() { - var inputs = { - columnWidths: document.getElementById('column-widths-input'), - columnMinWidths: document.getElementById('column-min-widths-input'), - columnMaxWidths: document.getElementById('column-max-widths-input'), - padding: document.getElementById('padding-input'), - paddingMin: document.getElementById('padding-min-input'), - paddingMax: document.getElementById('padding-max-input'), - currentDirectory: document.getElementById('current-directory-input'), - windowWidth: document.getElementById('window-width-input'), - windowHeight: document.getElementById('window-height-input'), - editorTextWidth: document.getElementById('editor-text-width-input'), - editorTextHeight: document.getElementById('editor-text-height-input'), - editorHexWidth: document.getElementById('editor-hex-width-input'), - editorHexHeight: document.getElementById('editor-hex-height-input') + loadSettings() { + const setInputValue = (inputId, value) => { + const input = document.getElementById(`${inputId}-input`); + if (!input) return; + let v; + if (typeof value == 'object') + input.value = Object.entries(value).map(([id, value]) => { + return `${id}:${value}` + }).join(','); + else + input.value = value; }; - // Populate the input fields with the current config values - if (inputs.columnWidths) { - inputs.columnWidths.value = Object.keys(config.columnWidths).map(function(field) { - return field + ':' + config.columnWidths[field]; - }).join(','); - } - if (inputs.columnMinWidths) { - inputs.columnMinWidths.value = Object.keys(config.columnMinWidths).map(function(field) { - return field + ':' + config.columnMinWidths[field]; - }).join(','); - } - if (inputs.columnMaxWidths) { - inputs.columnMaxWidths.value = Object.keys(config.columnMaxWidths).map(function(field) { - return field + ':' + config.columnMaxWidths[field]; - }).join(','); - } - if (inputs.padding) { - inputs.padding.value = config.padding; - } - if (inputs.paddingMin) { - inputs.paddingMin.value = config.paddingMin; - } - if (inputs.paddingMax) { - inputs.paddingMax.value = config.paddingMax; - } - if (inputs.currentDirectory) { - inputs.currentDirectory.value = config.currentDirectory || '/'; - } - if (inputs.windowWidth) { - inputs.windowWidth.value = config.windowSizes.width; - } - if (inputs.windowHeight) { - inputs.windowHeight.value = config.windowSizes.height; - } - if (inputs.editorTextWidth) { - inputs.editorTextWidth.value = config.editorContainerSizes.text.width; - } - if (inputs.editorTextHeight) { - inputs.editorTextHeight.value = config.editorContainerSizes.text.height; + Object.entries(config).forEach(([inputId, value]) => { + setInputValue(inputId, value); + }); + }, + + updateUI() { + const styleElement = document.querySelector('style'); + if (styleElement) { + styleElement.textContent = styleElement.textContent.replace(/padding: \d+px/g, `padding: ${config.padding}px`); } - if (inputs.editorHexWidth) { - inputs.editorHexWidth.value = config.editorContainerSizes.hex.width; + + const fileListContainer = document.getElementById('file-list-container'); + if (fileListContainer) { + fileListContainer.style.width = `${config.windowWidth}px`; + fileListContainer.style.height = `${config.windowHeight}px`; } - if (inputs.editorHexHeight) { - inputs.editorHexHeight.value = config.editorContainerSizes.hex.height; + + const editorContainer = document.getElementById('editor-container'); + if (editorContainer) { + const editorMode = this.editorMode; + const editorHeight = config[`${editorMode}editorHeight`] || 550; + const editorWidth = config[`${editorMode}editorWidth`] || 850; + editorContainer.style.width = `${editorWidth}px`; + editorContainer.style.height = `${editorHeight}px`; } }, - renderEditor: function(filePath) { - var self = this; + renderEditor(filePath) { + const self = this; - var editorContainer = document.getElementById('editor-container'); + const editorContainer = document.getElementById('editor-container'); // Clear the editor container editorContainer.innerHTML = ''; // Get the sizes from the config - var mode = self.editorMode; // 'text' or 'hex' - var editorSizes = config.editorContainerSizes[mode] || { - width: 850, - height: 550 - }; + const mode = self.editorMode; // 'text' or 'hex' + const editorHeight = config[`${mode}editorHeight`] || 550; + const editorWidth = config[`${mode}editorWidth`] || 850; // Create the editor content container - var editorContentContainer = E('div', { + const editorContentContainer = E('div', { 'class': 'editor-content', 'style': 'flex: 1; display: flex; overflow: hidden;' }, []); // Action buttons array - var actionButtons = []; + let actionButtons = []; if (mode === 'text') { // Create line numbers div (initially hidden) - var lineNumbersDiv = E('div', { + const lineNumbersDiv = E('div', { 'id': 'line-numbers', 'class': 'line-numbers', 'style': 'display: none;' // Initially hidden }, []); // Create textarea for text editing - var editorTextarea = E('textarea', { + const editorTextarea = E('textarea', { 'wrap': 'off', 'id': 'editor-textarea', 'style': 'flex: 1; resize: none; border: none; padding: 0; margin: 0; overflow: auto;' @@ -2750,7 +2514,7 @@ return view.extend({ // Add event listeners for updating line numbers and synchronizing scroll editorTextarea.addEventListener('input', self.updateLineNumbers.bind(self)); editorTextarea.addEventListener('scroll', self.syncScroll.bind(self)); - lineNumbersDiv.addEventListener('scroll', function() { + lineNumbersDiv.addEventListener('scroll', () => { editorTextarea.scrollTop = lineNumbersDiv.scrollTop; }); @@ -2758,7 +2522,7 @@ return view.extend({ actionButtons = [ E('button', { 'class': 'btn cbi-button-save custom-save-button', - 'click': function() { + 'click'() { self.handleSaveFile(filePath); } }, _('Save')), @@ -2766,7 +2530,7 @@ return view.extend({ 'class': 'btn', 'id': 'toggle-hex-mode', 'style': 'margin-left: 10px;', - 'click': function() { + 'click'() { self.toggleHexMode(filePath); } }, _('Toggle to Hex Mode')), @@ -2774,14 +2538,14 @@ return view.extend({ 'class': 'btn', 'id': 'toggle-line-numbers', 'style': 'margin-left: 10px;', - 'click': function() { + 'click'() { self.toggleLineNumbers(); } }, _('Toggle Line Numbers')) ]; } else if (mode === 'hex') { // Create hex editor container - var hexeditContainer = E('div', { + const hexeditContainer = E('div', { 'id': 'hexedit-container', 'style': 'flex: 1; overflow: hidden; display: flex; flex-direction: column;' }); @@ -2800,7 +2564,7 @@ return view.extend({ actionButtons = [ E('button', { 'class': 'btn cbi-button-save custom-save-button', - 'click': function() { + 'click'() { self.handleSaveFile(filePath); } }, _('Save')), @@ -2809,7 +2573,7 @@ return view.extend({ 'class': 'btn', 'id': 'toggle-text-mode', 'style': 'margin-left: 10px;', - 'click': function() { + 'click'() { self.toggleHexMode(filePath); } }, _('Toggle to ASCII Mode')) @@ -2818,9 +2582,9 @@ return view.extend({ } // Create the editor container with resizing and scrolling - var editor = E('div', { + const editor = E('div', { 'class': 'editor-container', - 'style': 'display: flex; flex-direction: column; width: ' + editorSizes.width + 'px; height: ' + editorSizes.height + 'px; resize: both; overflow: hidden;' + 'style': 'display: flex; flex-direction: column; width: ' + editorWidth + 'px; height: ' + editorHeight + 'px; resize: both; overflow: hidden;' }, [ editorContentContainer, E('div', { @@ -2832,17 +2596,17 @@ return view.extend({ editorContainer.appendChild(editor); // Update status bar and message - var statusInfo = document.getElementById('status-info'); + const statusInfo = document.getElementById('status-info'); if (statusInfo) { statusInfo.textContent = _('Editing: ') + filePath; } - var editorMessage = document.getElementById('editor-message'); + const editorMessage = document.getElementById('editor-message'); if (editorMessage) { editorMessage.textContent = _('Editing: ') + filePath; } // Clear any progress messages - var statusProgress = document.getElementById('status-progress'); + const statusProgress = document.getElementById('status-progress'); if (statusProgress) { statusProgress.innerHTML = ''; } @@ -2863,8 +2627,8 @@ return view.extend({ // Update config only if newWidth and newHeight are greater than 0 if (newWidth > 0 && newHeight > 0) { - config.editorContainerSizes[mode].width = newWidth; - config.editorContainerSizes[mode].height = newHeight; + config.editorWidth = newWidth; + config.editorHeight = newHeight; } } }); @@ -2879,7 +2643,7 @@ return view.extend({ * * @param {string} filePath - The path of the file to be edited. */ - toggleHexMode: function(filePath) { + toggleHexMode(filePath) { const self = this; if (self.editorMode === 'text') { diff --git a/applications/luci-app-filemanager/htdocs/luci-static/resources/view/system/filemanager/HexEditor.js b/applications/luci-app-filemanager/htdocs/luci-static/resources/view/system/filemanager/HexEditor.js index bb4f55b573..56786b8272 100644 --- a/applications/luci-app-filemanager/htdocs/luci-static/resources/view/system/filemanager/HexEditor.js +++ b/applications/luci-app-filemanager/htdocs/luci-static/resources/view/system/filemanager/HexEditor.js @@ -27,295 +27,295 @@ var hexeditCssContent = ` /* Hex Editor CSS Styles */ .hexview:focus, .textview:focus { - outline: none; - box-shadow: none; - border-right: 2px solid var(--clr-border); + outline: none; + box-shadow: none; + border-right: 2px solid var(--clr-border); } :root { - --span-spacing: 0.25ch; - --clr-background: #f5f5f5; - --clr-selected: #c9daf8; - --clr-selected-editing: #6d9eeb; - --clr-non-printable: #999999; - --clr-border: #000000; - --clr-offset: #666666; - --clr-header: #333333; - --clr-highlight: yellow; /* Unified highlight color for matches */ - --clr-cursor-active: blue; /* Active cursor base color */ - --clr-cursor-passive: lightblue; /* Passive cursor color */ - --animation-duration: 1s; /* Duration for blinking animation */ + --span-spacing: 0.25ch; + --clr-background: #f5f5f5; + --clr-selected: #c9daf8; + --clr-selected-editing: #6d9eeb; + --clr-non-printable: #999999; + --clr-border: #000000; + --clr-offset: #666666; + --clr-header: #333333; + --clr-highlight: yellow; /* Unified highlight color for matches */ + --clr-cursor-active: blue; /* Active cursor base color */ + --clr-cursor-passive: lightblue; /* Passive cursor color */ + --animation-duration: 1s; /* Duration for blinking animation */ } /* Apply box-sizing to all elements */ .hexedit *, .hexedit *::before, .hexedit *::after { - box-sizing: border-box; + box-sizing: border-box; } /* Main hex editor container */ .hexedit { - display: flex; - flex-direction: column; - flex: 1; /* Allow hexedit to expand */ - font-family: monospace; - font-size: 14px; - line-height: 1.2em; - background-color: var(--clr-background); - border: 1px solid var(--clr-border); - width: 100%; + display: flex; + flex-direction: column; + flex: 1; /* Allow hexedit to expand */ + font-family: monospace; + font-size: 14px; + line-height: 1.2em; + background-color: var(--clr-background); + border: 1px solid var(--clr-border); + width: 100%; } .hexedit:focus { - outline: none; + outline: none; } /* Headers container */ .hexedit-headers { - display: flex; - background-color: var(--clr-background); - border-bottom: 2px solid var(--clr-border); - font-family: monospace; + display: flex; + background-color: var(--clr-background); + border-bottom: 2px solid var(--clr-border); + font-family: monospace; } /* Header styles */ .offsets-header, .hexview-header, .textview-header { - display: flex; - align-items: center; - padding: 5px; - box-sizing: border-box; - font-weight: bold; - color: var(--clr-header); - border-right: 2px solid var(--clr-border); + display: flex; + align-items: center; + padding: 5px; + box-sizing: border-box; + font-weight: bold; + color: var(--clr-header); + border-right: 2px solid var(--clr-border); } .offsets-header { - width: 100px; /* Ensure alignment with .offsets */ - text-align: left; + width: 100px; /* Ensure alignment with .offsets */ + text-align: left; } .hexview-header { - width: calc(16 * 2ch + 20 * var(--span-spacing)); /* Increased width to match content */ - display: flex; + width: calc(16 * 2ch + 20 * var(--span-spacing)); /* Increased width to match content */ + display: flex; } .hexview-header span { - width: 2ch; - margin-right: var(--span-spacing); - text-align: center; + width: 2ch; + margin-right: var(--span-spacing); + text-align: center; } .hexview-header span:last-child { - margin-right: 0; + margin-right: 0; } .textview-header { - flex: 1; - margin-left: 10px; - text-align: left; + flex: 1; + margin-left: 10px; + text-align: left; } /* Content container */ .hexedit-content { - display: flex; - height: 100%; - flex: 1 1 auto; - overflow: auto; - position: relative; - border-top: 2px solid var(--clr-border); + display: flex; + height: 100%; + flex: 1 1 auto; + overflow: auto; + position: relative; + border-top: 2px solid var(--clr-border); } /* Columns */ .offsets, .hexview, .textview { - flex-shrink: 0; - display: block; - padding: 5px; - position: relative; - border-right: 2px solid var(--clr-border); + flex-shrink: 0; + display: block; + padding: 5px; + position: relative; + border-right: 2px solid var(--clr-border); } .offsets { - width: 100px; /* Increased width to match content */ - display: flex; - flex-direction: column; - text-align: left; + width: 100px; /* Increased width to match content */ + display: flex; + flex-direction: column; + text-align: left; } .offsets span { - display: block; - height: 1.2em; + display: block; + height: 1.2em; } .hexview { - width: calc(16 * 2ch + 20 * var(--span-spacing)); /* Increased width to match content */ - text-align: center; + width: calc(16 * 2ch + 20 * var(--span-spacing)); /* Increased width to match content */ + text-align: center; } .textview { - flex: 1; - margin-left: 10px; - text-align: left; - border-right: none; + flex: 1; + margin-left: 10px; + text-align: left; + border-right: none; } /* Line containers */ .hex-line, .text-line { - display: flex; - height: 1.2em; + display: flex; + height: 1.2em; } /* Byte spans */ .hex-line span, .text-line span { - width: 2ch; - margin-right: var(--span-spacing); - text-align: center; - display: inline-block; - cursor: default; + width: 2ch; + margin-right: var(--span-spacing); + text-align: center; + display: inline-block; + cursor: default; } .hex-line span:last-child, .hexview-header span:last-child, .text-line span:last-child { - margin-right: 0; + margin-right: 0; } /* Selections */ .selected { - background-color: var(--clr-selected); + background-color: var(--clr-selected); } .selected-editing { - background-color: var(--clr-selected-editing); + background-color: var(--clr-selected-editing); } .non-printable { - color: var(--clr-non-printable); + color: var(--clr-non-printable); } /* Remove individual scrollbars */ .offsets::-webkit-scrollbar, .hexview::-webkit-scrollbar, .textview::-webkit-scrollbar { - display: none; + display: none; } .offsets, .hexview, .textview { - scrollbar-width: none; /* For Firefox */ + scrollbar-width: none; /* For Firefox */ } /* Adjust overall layout */ .hexedit .offsets, .hexedit .hexview, .hexedit .textview { - border-right: 2px solid var(--clr-border); + border-right: 2px solid var(--clr-border); } .hexedit .textview { - border-right: none; + border-right: none; } /* Responsive adjustments */ @media (max-width: 768px) { - .hexedit { - font-size: 12px; - } + .hexedit { + font-size: 12px; + } - .offsets { - width: 120px; /* Adjust for smaller screens */ - } + .offsets { + width: 120px; /* Adjust for smaller screens */ + } - .hexview { - width: calc(16 * 2ch + 20 * var(--span-spacing)); - } + .hexview { + width: calc(16 * 2ch + 20 * var(--span-spacing)); + } } /* Search container styles */ .hexedit-search-container { - padding: 10px; - background-color: #f9f9f9; - border-bottom: 1px solid #ccc; /* Border to separate from headers */ - display: flex; - flex-direction: column; /* Stack search groups vertically */ - gap: 10px; - width: 100%; - box-sizing: border-box; + padding: 10px; + background-color: #f9f9f9; + border-bottom: 1px solid #ccc; /* Border to separate from headers */ + display: flex; + flex-direction: column; /* Stack search groups vertically */ + gap: 10px; + width: 100%; + box-sizing: border-box; } /* Search group styles */ .hexedit-search-group { - display: flex; - align-items: center; - gap: 5px; - width: 100%; + display: flex; + align-items: center; + gap: 5px; + width: 100%; } /* Search input fields */ .hexedit-search-input { - flex: 1; - padding: 8px; - border: 1px solid #ddd; - border-radius: 4px; - font-size: 14px; + flex: 1; + padding: 8px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 14px; } /* Search status fields */ .hexedit-search-status { - width: 50px; - text-align: center; - font-size: 14px; - color: #555; + width: 50px; + text-align: center; + font-size: 14px; + color: #555; } /* Find Previous and Next buttons */ .hexedit-search-button { - padding: 8px 12px; - cursor: pointer; - background-color: #007bff; - color: white; - border: none; - border-radius: 4px; - font-size: 14px; - transition: background-color 0.3s ease; + padding: 8px 12px; + cursor: pointer; + background-color: #007bff; + color: white; + border: none; + border-radius: 4px; + font-size: 14px; + transition: background-color 0.3s ease; } .hexedit-search-button:hover { - background-color: #0056b3; + background-color: #0056b3; } /* Highlight search results */ .search-highlight { - background-color: var(--clr-highlight); + background-color: var(--clr-highlight); } /* Define keyframes for blinking blue */ @keyframes blink-blue { - 0% { background-color: var(--clr-cursor-active); } - 50% { background-color: white; } - 100% { background-color: var(--clr-cursor-active); } + 0% { background-color: var(--clr-cursor-active); } + 50% { background-color: white; } + 100% { background-color: var(--clr-cursor-active); } } /* Classes for active view cursor blinking */ .active-view-cursor { - animation: blink-blue var(--animation-duration) infinite; - background-color: var(--clr-cursor-active); /* Initial color */ + animation: blink-blue var(--animation-duration) infinite; + background-color: var(--clr-cursor-active); /* Initial color */ } /* Classes for passive view cursor highlighting */ .passive-view-cursor { - background-color: var(--clr-cursor-passive); + background-color: var(--clr-cursor-passive); } /* Highlighted class to maintain yellow background for matches */ .highlighted { - background-color: var(--clr-highlight); + background-color: var(--clr-highlight); } `; @@ -575,7 +575,7 @@ class HexEditor { pattern = document.getElementById('hexedit-search-regex').value.trim(); break; default: - console.warn(`Unknown search type: ${searchType}`); + // console.warn(`Unknown search type: ${searchType}`); pattern = ''; break; } @@ -588,7 +588,7 @@ class HexEditor { if (!pattern) { // Update status field to 0/0 this.updateSearchStatus(searchType, 0, 0); - console.log('No search pattern entered.'); + // console.log('No search pattern entered.'); return; } @@ -602,7 +602,7 @@ class HexEditor { this.searchRegex(pattern); } } catch (error) { - console.log(`Error during search: ${error.message}`); + // console.log(`Error during search: ${error.message}`); // Update status field to 0/0 on error this.updateSearchStatus(searchType, 0, 0); return; @@ -615,11 +615,11 @@ class HexEditor { this.navigateToMatch(this.currentMatchIndex); // Update status field with actual match count this.updateSearchStatus(searchType, this.currentMatchIndex + 1, this.matches.length); - console.log(`Found ${this.matches.length} matches.`); + // console.log(`Found ${this.matches.length} matches.`); } else { // Update status field to 0/0 if no matches found this.updateSearchStatus(searchType, 0, 0); - console.log('No matches found.'); + // console.log('No matches found.'); } } @@ -654,13 +654,13 @@ class HexEditor { if (this.matches.length === 0) { // Update status field to 0/0 if no matches this.updateSearchStatus(this.currentSearchType, 0, 0); - console.log('No matches to navigate.'); + // console.log('No matches to navigate.'); return; } // Ensure matchIndex is within bounds if (matchIndex < 0 || matchIndex >= this.matches.length) { - console.log('navigateToMatch: matchIndex out of bounds.'); + // console.log('navigateToMatch: matchIndex out of bounds.'); return; } @@ -672,7 +672,7 @@ class HexEditor { // Set selected index to the match start this.setSelectedIndex(match.index); - console.log(`Navigated to match ${matchIndex + 1} at offset ${match.index.toString(16)}`); + // console.log(`Navigated to match ${matchIndex + 1} at offset ${match.index.toString(16)}`); // Update status field this.updateSearchStatus(this.currentSearchType, this.currentMatchIndex + 1, this.matches.length); @@ -697,7 +697,7 @@ class HexEditor { regex.lastIndex++; } } - console.log(`searchASCII: Found ${this.matches.length} matches.`); + // console.log(`searchASCII: Found ${this.matches.length} matches.`); } /** @@ -733,7 +733,7 @@ class HexEditor { }); } } - console.log(`searchHEX: Found ${this.matches.length} matches.`); + // console.log(`searchHEX: Found ${this.matches.length} matches.`); } /** @@ -757,7 +757,7 @@ class HexEditor { regex.lastIndex++; } } - console.log(`searchRegex: Found ${this.matches.length} matches.`); + // console.log(`searchRegex: Found ${this.matches.length} matches.`); } /** @@ -772,9 +772,9 @@ class HexEditor { // Calculate new scroll position to ensure the matched line is visible const newScrollTop = Math.max(0, (lineNumber * lineHeight) - ((this.visibleRows / 2) * lineHeight)); - console.log(`scrollToMatch called with index: ${index}`); - console.log(`lineNumber: ${lineNumber}`); - console.log(`newScrollTop: ${newScrollTop}`); + // console.log(`scrollToMatch called with index: ${index}`); + // console.log(`lineNumber: ${lineNumber}`); + // console.log(`newScrollTop: ${newScrollTop}`); // Update the scrollTop property to trigger handleScroll this.hexeditContent.scrollTop = newScrollTop; @@ -809,7 +809,7 @@ class HexEditor { const containerHeight = this.hexeditContent.clientHeight; this.visibleRows = Math.floor(containerHeight / lineHeight); this.visibleByteCount = this.bytesPerRow * this.visibleRows; - console.log(`calculateVisibleRows: visibleRows=${this.visibleRows}, visibleByteCount=${this.visibleByteCount}`); + // console.log(`calculateVisibleRows: visibleRows=${this.visibleRows}, visibleByteCount=${this.visibleByteCount}`); this.renderDom(); // Re-render to apply the new rows } @@ -821,7 +821,7 @@ class HexEditor { setData(data) { this.data = data; this.totalRows = Math.ceil(this.data.length / this.bytesPerRow); - console.log(`setData: data length=${this.data.length}, totalRows=${this.totalRows}`); + // console.log(`setData: data length=${this.data.length}, totalRows=${this.totalRows}`); this.calculateVisibleRows(); // Ensure visibleRows are calculated before rendering } @@ -845,13 +845,13 @@ class HexEditor { const firstVisibleLine = Math.floor(scrollTop / lineHeight); const newStartIndex = firstVisibleLine * this.bytesPerRow; - console.log(`handleScroll: scrollTop=${scrollTop}, firstVisibleLine=${firstVisibleLine}, newStartIndex=${newStartIndex}`); + // console.log(`handleScroll: scrollTop=${scrollTop}, firstVisibleLine=${firstVisibleLine}, newStartIndex=${newStartIndex}`); // Update startIndex and re-render the DOM if necessary if (newStartIndex !== this.startIndex) { this.startIndex = newStartIndex; this.renderDom(); // Re-render visible data - console.log(`handleScroll: Updated startIndex and rendered DOM.`); + // console.log(`handleScroll: Updated startIndex and rendered DOM.`); } } @@ -1163,7 +1163,7 @@ class HexEditor { */ setSelectedIndex(index) { this.selectedIndex = index; - console.log(`setSelectedIndex called with index: ${index}`); + // console.log(`setSelectedIndex called with index: ${index}`); if (index !== null) { // Calculate the line number of the selected index @@ -1175,13 +1175,13 @@ class HexEditor { const visibleStartLine = Math.floor(this.hexeditContent.scrollTop / lineHeight); const visibleEndLine = visibleStartLine + this.visibleRows; - console.log(`setSelectedIndex: lineNumber=${lineNumber}, visibleStartLine=${visibleStartLine}, visibleEndLine=${visibleEndLine}`); + // console.log(`setSelectedIndex: lineNumber=${lineNumber}, visibleStartLine=${visibleStartLine}, visibleEndLine=${visibleEndLine}`); // If the selected line is out of the visible range, update scrollTop if (lineNumber < visibleStartLine || lineNumber >= visibleEndLine) { const newScrollTop = Math.max(0, (lineNumber * lineHeight) - ((this.visibleRows / 2) * lineHeight)); this.hexeditContent.scrollTop = newScrollTop; - console.log(`setSelectedIndex: Updated scrollTop to ${this.hexeditContent.scrollTop}`); + // console.log(`setSelectedIndex: Updated scrollTop to ${this.hexeditContent.scrollTop}`); } } @@ -1298,8 +1298,8 @@ return L.Class.extend({ /** * Initializing new instance of HexEditor. * - * @param {HTMLElement} hexeditDomObject - DOM элемент для HexEditor. - * @returns {HexEditor} - Новый экземпляр HexEditor. + * @param {HTMLElement} hexeditDomObject - DOM element for HexEditor. + * @returns {HexEditor} - new instance of HexEditor. */ initialize: function(hexeditDomObject) { return new HexEditor(hexeditDomObject); diff --git a/applications/luci-app-filemanager/htdocs/luci-static/resources/view/system/filemanager/md.js b/applications/luci-app-filemanager/htdocs/luci-static/resources/view/system/filemanager/md.js index 70e9693977..48100fbf89 100644 --- a/applications/luci-app-filemanager/htdocs/luci-static/resources/view/system/filemanager/md.js +++ b/applications/luci-app-filemanager/htdocs/luci-static/resources/view/system/filemanager/md.js @@ -13,87 +13,105 @@ * @returns {string} - The resulting HTML string. */ function parseMarkdown(markdown) { - // Split the input into lines const lines = markdown.split('\n'); const html = []; - let inList = false; - let listType = ''; // 'ul' or 'ol' - - lines.forEach((line) => { - let trimmedLine = line.trim(); - - if (trimmedLine === '') { - // Empty line signifies a new paragraph - if (inList) { - html.push(``); - inList = false; - listType = ''; - } - return; // Skip adding empty lines to HTML + + // Stack of open lists: [{ type: "ul"|"ol", indent: number }] + const listStack = []; + + function closeListsToIndent(indent) { + while (listStack.length > 0 && listStack[listStack.length - 1].indent >= indent) { + const last = listStack.pop(); + html.push(``); + } + } + + function openList(type, indent, startNumber = null) { + listStack.push({ type, indent }); + if (type === "ol" && startNumber != null && startNumber !== 1) + html.push(`
    `); + else + html.push(`<${type}>`); + } + + lines.forEach(line => { + // Detect indentation level (2 spaces = one indent) + const indentSpaces = line.match(/^ */)[0].length; + const indent = Math.floor(indentSpaces / 2); + + const trimmed = line.trim(); + if (trimmed === "") { + // Close all lists for blank lines, do NOT output

    + closeListsToIndent(0); + return; } - // Check for headings - if (/^###\s+(.*)/.test(trimmedLine)) { - const content = trimmedLine.replace(/^###\s+/, ''); - html.push(`

    ${escapeHtml(content)}

    `); + // -------- + // Headings + // -------- + if (/^###\s+/.test(trimmed)) { + closeListsToIndent(0); + html.push(`

    ${escapeHtml(trimmed.replace(/^###\s+/, ''))}

    `); return; - } else if (/^##\s+(.*)/.test(trimmedLine)) { - const content = trimmedLine.replace(/^##\s+/, ''); - html.push(`

    ${escapeHtml(content)}

    `); + } + if (/^##\s+/.test(trimmed)) { + closeListsToIndent(0); + html.push(`

    ${escapeHtml(trimmed.replace(/^##\s+/, ''))}

    `); return; - } else if (/^#\s+(.*)/.test(trimmedLine)) { - const content = trimmedLine.replace(/^#\s+/, ''); - html.push(`

    ${escapeHtml(content)}

    `); + } + if (/^#\s+/.test(trimmed)) { + closeListsToIndent(0); + html.push(`

    ${escapeHtml(trimmed.replace(/^#\s+/, ''))}

    `); return; } - // Check for ordered lists - let orderedMatch = trimmedLine.match(/^(\d+)\.\s+(.*)/); - if (orderedMatch) { - const [, number, content] = orderedMatch; - if (!inList || listType !== 'ol') { - if (inList) { - html.push(``); - } - html.push('
      '); - inList = true; - listType = 'ol'; + // ------------------------ + // Ordered lists: "N. text" + // ------------------------ + let mOrdered = trimmed.match(/^(\d+)\.\s+(.*)/); + if (mOrdered) { + const num = parseInt(mOrdered[1], 10); + const content = mOrdered[2]; + + const last = listStack[listStack.length - 1]; + + if (!last || last.indent < indent || last.type !== "ol") { + // NEW ordered list + closeListsToIndent(indent); + openList("ol", indent, num); } + // ELSE: same indent, same list → continue existing OL without closing/opening + html.push(`
    1. ${parseInlineMarkdown(escapeHtml(content))}
    2. `); return; } - // Check for unordered lists - let unorderedMatch = trimmedLine.match(/^[-*]\s+(.*)/); - if (unorderedMatch) { - const content = unorderedMatch[1]; - if (!inList || listType !== 'ul') { - if (inList) { - html.push(``); - } - html.push('
        '); - inList = true; - listType = 'ul'; + // ------------------------------------- + // Unordered lists: "- text" or "* text" + // ------------------------------------- + let mUnordered = trimmed.match(/^[-*]\s+(.*)/); + if (mUnordered) { + const content = mUnordered[1]; + const last = listStack[listStack.length - 1]; + + if (!last || last.indent < indent || last.type !== "ul") { + closeListsToIndent(indent); + openList("ul", indent); } + html.push(`
      • ${parseInlineMarkdown(escapeHtml(content))}
      • `); return; } - // If currently inside a list but the line doesn't match a list item, close the list - if (inList) { - html.push(``); - inList = false; - listType = ''; - } - - // Regular paragraph - html.push(`

        ${parseInlineMarkdown(escapeHtml(trimmedLine))}

        `); + // --------- + // Paragraph + // --------- + closeListsToIndent(0); + html.push(`

        ${parseInlineMarkdown(escapeHtml(trimmed))}

        `); }); - // Close any open list tags at the end - if (inList) { - html.push(``); - } + // Close all remaining lists + closeListsToIndent(0); return html.join('\n'); } diff --git a/applications/luci-app-filemanager/htdocs/luci-static/resources/view/system/filemanager/md_help.js b/applications/luci-app-filemanager/htdocs/luci-static/resources/view/system/filemanager/md_help.js index 0e4275d736..fd21c065a4 100644 --- a/applications/luci-app-filemanager/htdocs/luci-static/resources/view/system/filemanager/md_help.js +++ b/applications/luci-app-filemanager/htdocs/luci-static/resources/view/system/filemanager/md_help.js @@ -17,7 +17,7 @@ The **LuCI OpenWrt File Manager** is a tool to navigate directories, manage file 2. **File Management** - **View Files and Directories**: Display a list of files and folders within the current directory. - **Navigate Directories**: Move into subdirectories or return to parent directories. - - **Resizable Columns**: Adjust the width of table columns to enhance readability and organization. + - **Resizeable Columns**: Adjust the width of table columns to enhance readability and organization. - **Drag-and-Drop Uploads**: Upload files by simply dragging them into the designated area. - **Upload via File Selector**: Use the "Upload File" button to select and upload files from your local machine. - **Create New Files and Folders**: @@ -118,7 +118,7 @@ The **LuCI OpenWrt File Manager** is a tool to navigate directories, manage file ## Additional Functionalities -- **Resizable Columns and Windows**: Enhance the interface's flexibility by resizing table columns and editor windows to match your workflow. The Help window starts at **650x600** pixels and can be adjusted as needed. +- **Resizeable Columns and Windows**: Enhance the interface's flexibility by resizing table columns and editor windows to match your workflow. The Help window starts at **650x600** pixels and can be adjusted as needed. - **Responsive Design**: The application adapts to different screen sizes, ensuring usability across various devices. - **Error Handling and Notifications**: Receive immediate feedback on actions, helping you stay informed about the status of your file management tasks. - **Line Number Toggle**: Easily show or hide line numbers in the text editor to assist with content navigation. -- 2.30.2